Class: SvgSentinel::Scanner

Inherits:
Object
  • Object
show all
Includes:
Rules
Defined in:
lib/svg_sentinel/scanner.rb

Overview

The scanner. It reads an SVG string and reports what makes it unsafe.

The rules come from two public sources: general SVG cross-site-scripting knowledge (no scripts, no event handlers, no javascript: URIs, no foreign objects, no XXE) and the BIMI "SVG Tiny 1.2 Portable/Secure" profile used for brand-mark logos (no scripting, no animation, no external references, no raster images). Nothing here is fetched over the network.

It is built to survive hostile input: input is normalised to valid UTF-8 first, a hard byte cap and streaming structural limits refuse oversized or pathologically shaped payloads before the tree parser runs, DOCTYPE / ENTITY declarations are refused so the parser is never exposed to entity-expansion or external-entity attacks, and every parse failure becomes a finding rather than an exception.

Raw findings are re-rated by a Policy for the rendering context and any configured severity overrides before they reach the Result.

Constant Summary collapse

DEFAULT_MAX_BYTES =

BIMI recommends brand-mark SVGs stay small.

32_768
DEFAULT_HARD_MAX_BYTES =

5 MiB: refuse to parse anything larger.

5_242_880
DEFAULT_MAX_DEPTH =
100
DEFAULT_MAX_NODES =
200_000
DEFAULT_MAX_ATTRIBUTES =
512
USE_EXPANSION_LIMIT =

Nested can expand exponentially ("SVG use bomb"). If the fully expanded node count would exceed this, the SVG is a render-DoS risk.

1_000_000

Constants included from Rules

Rules::ANIMATION_ELEMENTS, Rules::DANGEROUS_SCHEMES, Rules::EXTERNAL_SCHEMES, Rules::FOREIGN_ELEMENTS, Rules::SCRIPTABLE_DATA_MEDIA, Rules::SCRIPT_ELEMENTS, Rules::SENSITIVE_ANIMATED_ATTRIBUTES, Rules::STRICT_ALLOWED_ELEMENTS, Rules::URI_ATTRIBUTES

Instance Method Summary collapse

Methods included from Rules

allowed_element?, animation_element?, classify_reference, css_reference_targets, dangerous_css?, dangerous_scheme?, data_uri_media_type, decontrol, event_handler_attr?, external_scheme?, foreign_element?, script_element?, scriptable_data?, sensitive_animation_target?, strip_css_comments, to_utf8, uri_attribute?, uri_scheme

Constructor Details

#initialize(profile: :strict, allow_external: false, allow_data_uri: false, max_bytes: DEFAULT_MAX_BYTES, hard_max_bytes: DEFAULT_HARD_MAX_BYTES, max_depth: DEFAULT_MAX_DEPTH, max_nodes: DEFAULT_MAX_NODES, max_attributes: DEFAULT_MAX_ATTRIBUTES, context: :inline, severity_overrides: {}, disabled: []) ⇒ Scanner

Returns a new instance of Scanner.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/svg_sentinel/scanner.rb', line 43

def initialize(profile: :strict, allow_external: false, allow_data_uri: false,
  max_bytes: DEFAULT_MAX_BYTES, hard_max_bytes: DEFAULT_HARD_MAX_BYTES,
  max_depth: DEFAULT_MAX_DEPTH, max_nodes: DEFAULT_MAX_NODES,
  max_attributes: DEFAULT_MAX_ATTRIBUTES,
  context: :inline, severity_overrides: {}, disabled: [])
  @profile = profile
  @allow_external = allow_external
  @allow_data_uri = allow_data_uri
  # A limit of 0 (or negative, or nil) means "disabled" - the single place
  # this is decided, so config, CLI, and the library agree.
  @max_bytes = limit(max_bytes)
  @hard_max_bytes = limit(hard_max_bytes)
  @max_depth = limit(max_depth)
  @max_nodes = limit(max_nodes)
  @max_attributes = limit(max_attributes)
  @policy = Policy.new(context: context, overrides: severity_overrides, disabled: disabled)
  @findings = []
  @current_path = nil
end

Instance Method Details

#scan(svg) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/svg_sentinel/scanner.rb', line 63

def scan(svg)
  @findings = []
  @current_path = nil
  collect(svg)
  Result.new(@policy.apply(@findings))
rescue SystemStackError
  # Recursion (parser or tree walk) overflowed the stack on pathologically
  # nested input. Only reachable when structural limits are disabled; the
  # contract is still that scan returns a Result, never raises.
  @current_path = nil
  add_security(:too_deep, :critical, "SVG nesting overflowed the stack")
  Result.new(@policy.apply(@findings))
end