Module: CamaleonCms::SvgContentChecker
- Defined in:
- lib/camaleon_cms/svg_content_checker.rb
Overview
Parse-based rejection for uploads a browser parses as markup.
Named for SVG because that is where it started; it now serves every markup extension the
scanner routes here (see UploaderContentSecurity::MARKUP_EXTENSIONS). The reason is the reason
it replaced the regex denylist for SVG: event handlers are rejected by shape — any attribute
whose name begins with on — so no handler name is ever enumerated, and a handler the web
platform ships next year is refused without a code change. A name list cannot make that promise;
the one in ContentSecurity is missing onpointer*, ontouch* and onauxclick today.
Constant Summary collapse
- SVG_BANNED_TAGS =
animateandsetare intentionally absent: SMIL animation elements carry no script by themselves, and their scripting vector is the onbegin/onend/onrepeat attribute, which the element-agnostic on* check below rejects wherever it appears. foreignObject and handler stay banned — they embed foreign markup or handlers.form/meta/base/style/link are valid in SVG and none executes script on its own, but an uploaded SVG is served inline from the site origin and these five are what turn a passive image into markup that can navigate (meta http-equiv=refresh, base href), collect input (form), or pull in remote styling (link, style). This list only ever runs for uploads that are being scanned at all, i.e. from uploaders without
media_unfiltered_upload, so it needs no trust argument. %w[ script foreignObject iframe object embed handler form meta base style link ].freeze
- BANNED_TAGS =
What the checker actually refuses: the SVG list above unioned with the generic ruleset's element denylist. The union is load-bearing, not tidiness. ContentSecurity::BLOCKED_ELEMENTS refuses
applet frameset frame template portal marquee math, which SVG_BANNED_TAGS does not; routing.htmlhere while checking only the SVG list would have narrowed what an HTML upload is refused for. Unioning means every extension routed here is refused for at least everything the ruleset it came from refused. (SVG_BANNED_TAGS | ContentSecurity::BLOCKED_ELEMENTS).freeze
- HTML_MODE =
Extensions parsed as HTML rather than XML. Kept here rather than inferred, because the two modes differ in what a parse failure means (see
parse_document). :html- XML_MODE =
:xml- ASCII_UPPER =
Lowercasing tables for XPath translate(). The tag check folds case so a name an HTML parser lowercases is matched the same as its source form:
Nokogiri::HTMLreports<foreignObject>asforeignobject, and an SVG inlined into an HTML document fires<SCRIPT>exactly as<script>. Without folding,foreignObject-- the only mixed-case entry in BANNED_TAGS -- would be refused as.svg(XML, case-sensitive) yet accepted as.html. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'- ASCII_LOWER =
'abcdefghijklmnopqrstuvwxyz'- BANNED_TAGS_XPATH =
The banned-element XPath, built once from the frozen BANNED_TAGS rather than reassembled on every scan:
//*[translate(local-name(),UPPER,LOWER)='tag' or ...], each tag folded to lowercase so an HTML-lowercased element name still matches. "//*[#{BANNED_TAGS.map do |tag| "translate(local-name(), '#{ASCII_UPPER}', '#{ASCII_LOWER}') = '#{tag.downcase}'" end.join(' or ')}]".freeze
Class Method Summary collapse
-
.unsafe?(content, mode: XML_MODE) ⇒ Boolean
mode:defaults to :xml so the pre-existing single-argument call (unsafe?(content)) keeps its exact behaviour for any downstream caller.
Class Method Details
.unsafe?(content, mode: XML_MODE) ⇒ Boolean
mode: defaults to :xml so the pre-existing single-argument call (unsafe?(content)) keeps
its exact behaviour for any downstream caller.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/camaleon_cms/svg_content_checker.rb', line 60 def unsafe?(content, mode: XML_MODE) return true if content.nil? || content.empty? # rubocop:disable Rails/Blank doc = parse_document(content, mode) return true if doc.nil? # XML syntax error -- fail closed # XML mode only. A document that parses to nothing is not a valid document of its format, so # refusing it fails closed. HTML has no such signal — Nokogiri::HTML never raises and reports # a nil root for input as ordinary as a whitespace-only or comment-only file, which must be # accepted. See design.md D3: the guarantee does not exist in HTML mode and is not faked. return true if mode != HTML_MODE && doc.root.nil? dangerous_document?(doc, mode) end |