Skip to content

Sanitizing

phil/svg-sanitizer

An image

SvgSanitizer::sanitize() takes the content of an .svg file, or an <svg> element written as XML, and returns a Sanitized:

$sanitized = $sanitizer->sanitize(file_get_contents($upload->getPathname()));

if ($sanitized->isRefused()) {
    // Nothing to store: the one removal says why.
}

file_put_contents($target, $sanitized->svg);
MemberMeaning
svgThe image: an <svg> element in the SVG namespace, without XML declaration; empty when it is refused whole
removalsEach Removal, in the order of the source, up to Sanitized::MAX_REMOVALS (100)
omittedRemovalsThe removals past the first 100, counted but not listed
isClean()Whether nothing was removed: the image was allowed as it was
isRefused()Whether the image was refused whole

The output is built from an empty document: each element of the source the policy allows is created again, with the attributes the policy allows and its text; everything else is left behind. Comments, the XML declaration and the document type are left out without a removal. An image whose root element has no namespace is read as SVG and written in the SVG namespace, as an <svg> taken out of an HTML page often is.

An element that is removed takes its content with it: the text of a <script>, the children of a <metadata> or of an element of another namespace never reach the output.

Removals

An image of a few megabytes can hold hundreds of thousands of attributes to remove: the first 100 removals are listed, in the order of the source, those of its CSS among them, and omittedRemovals counts the others. An image with removals past the first 100 is no more refused than one with fewer.

A Removal holds a RemovalReason, a subject — the element or attribute name, the address, or the attribute and the CSS subject, cut to 80 characters — and the line of the source where its element starts.

ReasonSubjectWhat is removed
Elementmetadata, i:namedAn element the policy does not allow, or one of another namespace, with its content
DeniedElementscriptAn element that runs code or embeds another document, with its content
Attributedata-x, xml:baseAn attribute the policy does not allow, of another namespace, or with an undeclared prefix
EventHandleronclickAn event handler
ExternalReferenceother.svg#a, fill: http://x.test/pAn href or a url() that is not a fragment
DangerousUrljavascript:alert(1)An address whose scheme runs code or holds a document
Linkftp://x.test/A link whose scheme the policy does not allow, or a relative link
Animationset href, to: url(x)An animation of a reference, a style or a handler, with its element; a value of animation holding a function or a scheme
Stylestyle: positionCSS the CSS policy removed, under the attribute or element that held it
ProcessingInstructionxml-stylesheetA processing instruction

Some reasons refuse the image whole: svg is empty, and the removal is the only one. RemovalReason::refusesWhole() tells them apart.

ReasonWhen
TooLargeThe image is larger than maxBytes
EntityDeclarationThe image declares an entity
MalformedThe image is not well-formed XML in UTF-8, holds a null byte, or is empty
NotSvgThe root element is not <svg>, in the SVG namespace or in none
TooManyElementsThe image holds more than maxElements elements, kept or not
TooManyAttributesAn element holds more than maxAttributes attributes, kept or not; counted before the image is parsed
TooDeepThe elements kept nest deeper than maxDepth

describe() gives each reason in a few words, for a message:

foreach ($sanitized->removals as $removal) {
    $messages[] = sprintf('Line %d: %s (%s).', $removal->line, ucfirst($removal->reason->describe()), $removal->subject);
}