Skip to main content

svg-sanitize — restrictive, visual-only SVG sanitizer (prototype)

svg-sanitize rebuilds untrusted SVG as static vector graphics only. It does not simply search and replace script tags. It parses XML with defusedxml, checks resource limits, and constructs an entirely new XML tree from a small, explicit allowlist of elements, attributes, and valid values.

Prototype, not a formally verified security boundary. Review, fuzz and test against your target SVG renderers before using this in a security-sensitive production system.

Why svg-sanitize?

The svg-sanitize project started following security reviews conducted by Jeroen Pinoy, which highlighted the potential security risks associated with handling untrusted SVG files. This initial prototype was done during the hack.lu 2026 hackathon after the hot ones interview ;-)

Although SVG is primarily an image format, it can contain active content, including JavaScript, event handlers, external resource references, and other potentially malicious elements. This makes SVG a potential attack vector, particularly when displaying user-supplied images in web applications.

svg-sanitize is a lightweight Python library designed to transform untrusted SVG files into passive, visual-only SVG documents. Instead of attempting to identify every possible malicious payload, the library follows a strict allowlist approach, preserving only elements and attributes required for static vector graphics.

The objective is to provide a simple, reusable, and open-source security component that reduces the attack surface when processing SVG files, without requiring applications to implement their own SVG sanitization logic.

Installation

python -m pip install -e './svg-sanitize[test]'
# or, when inside the extracted directory:
python -m pip install -e '.[test]'

Python 3.10+; runtime dependencies are defusedxml for XML parsing and tinycss2 for parsing inline declarations in visual mode.

Python usage

from svg_sanitize import sanitize_svg, SVGValidationError

untrusted_svg = b'''<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">
  <script>alert(1)</script>
  <rect width="80" height="40" fill="red"/>
</svg>'''

try:
    passive_svg = sanitize_svg(untrusted_svg)
except SVGValidationError as exc:
    print(f"SVG rejected: {exc}")
else:
    print(passive_svg)
    # <svg xmlns="http://www.w3.org/2000/svg"><rect width="80" height="40" fill="red" /></svg>

For custom input and rendering limits:

from svg_sanitize import Limits, sanitize_svg

output = sanitize_svg(untrusted_svg, limits=Limits(max_input_bytes=100_000, max_nodes=1_000))

Choosing a mode

mode="strict" is the default and preserves the original minimal policy. Use mode="visual" for artwork that stores its presentation in inline styles, such as the Luxembourg map:

output = sanitize_svg(untrusted_svg, mode="visual")
Feature strict (default) visual
Validated primitive geometry and presentation attributes Preserved Preserved
Inline style declarations Removed Allowlisted values converted to presentation attributes; declaration order and !important are respected
Local markers (arrowheads, railway ticks) Removed Sanitized defs/marker subset, rewritten IDs and same-document marker references
Fonts, units, text spacing Minimal subset Additional known local font families, physical/font-relative units, font-stretch, and xml:space
Scripts, events, links, animation, external resources, embedded images, CSS stylesheets Removed Removed
Gradients, patterns, filters, clipping, masks, symbols, <use> Removed Removed

Visual mode does not copy CSS or arbitrary markup through. Unsupported declarations, selectors, CSS variables and expressions are dropped. Marker definitions cannot reference other resources or inherit marker references; duplicate/invalid IDs and unresolved references are discarded. Additional Limits fields bound each marker to 128 sanitized nodes and the document to 256 explicit marker reference attributes by default. These bounds do not constitute a renderer resource sandbox.

The full Luxembourg map has a rendered comparison and an Inkscape regression test. Visual mode restores its styling and markers; it does not promise identical appearance for arbitrary SVGs or renderers.

CLI

svg-sanitize examples/malicious.svg -o clean.svg
# alternatively:
python -m svg_sanitize examples/malicious.svg -o clean.svg
# preserve validated inline styling and local markers:
svg-sanitize map.svg --mode visual -o map.clean.svg

Run tests:

python -m pytest -q

To run the optional full-map render comparison, install Inkscape and DejaVu fonts (on Debian/Ubuntu: apt-get install inkscape fonts-dejavu-core), then:

python -m pip install -e '.[test,render]'
python -m pytest -q tests/test_visual_rendering.py

The regular test matrix skips this comparison when Pillow or Inkscape is absent; a dedicated CI job installs them and runs it. The comparison renders only the checksum-pinned original and sanitizer outputs, never the attack fixtures.

CI and PyPI releases

The GitHub Actions workflow runs the tests on Python 3.10–3.14 for pull requests, pushes to main, and published GitHub releases. After the tests pass, it builds and checks both the source distribution and wheel. Only a published release triggers an upload to PyPI, using those checked artifacts. Publishing also requires the dedicated visual comparison job to pass.

Before the first release:

  1. Create a GitHub Actions environment named pypi in this repository. Add any desired deployment protection rules there.
  2. Configure a PyPI trusted publisher for the svg-sanitize project with owner adulau, repository svg-sanitize, workflow filename ci.yml, and environment name pypi. If the project does not exist yet, configure a pending publisher in your PyPI account instead.

No PyPI API token or GitHub repository secret is required. For each release, update project.version in pyproject.toml, merge the change, then create and publish a GitHub release from that commit with a matching tag, such as v0.1.0 or 0.1.0. A mismatched tag fails before publishing. Each upload must use a new version because PyPI does not allow replacing previously uploaded files. Published prereleases also run this workflow; drafts do not.

Luxembourg SVG regression corpus

The fixture corpus includes the original Wikimedia Luxembourg map and standalone attack variants derived from its region outlines. It covers active content, external references, XML attacks, invalid values, and resource limits, with offline library and CLI regression tests:

python -m pytest -q tests/test_luxembourg.py

Treat the attack fixtures as untrusted SVG. The map artwork and derivatives are CC BY-SA 4.0; attribution and license details are included with the corpus.

What is allowed

Strict mode allows svg, g, rect, circle, ellipse, line, polyline, polygon, path, text, and tspan. Numeric dimensions and coordinates, path data, point lists, limited transforms, limited named/hex/rgb colors, static stroke/ fill and selected text formatting properties are validated individually.

SVGs using gradients, filters, clipping paths, <use>, images, embedded fonts, CSS, masks, symbols, hyperlinks, animation or scripting will lose those features. In particular, gradient fills fill="url(#id)" are discarded, including local URLs. Visual mode additionally allows the validated styling and marker subset described above; the remaining unsupported features are dropped in both modes.

What is forbidden

  • JavaScript and scriptable elements; onload, onclick, and all event handlers.
  • CSS stylesheets (<style>), @import, and expressions. Strict mode removes style=; visual mode converts only supported inline declarations.
  • All links and resources, including href, xlink:href, external and internal references, data: URLs, <image>, <use>, embedded fonts, filters and gradients, except visual mode's validated same-document marker references.
  • <foreignObject> and arbitrary/HTML/XML namespace elements.
  • SVG animation and SMIL (animate, set, animateTransform, etc.).
  • DTDs, XML entity declarations, external entity references, malformed XML.
  • Unknown attributes (including class and xml:base). Input IDs are removed; visual mode assigns new IDs to permitted marker definitions.

Unknown elements are removed with their complete subtrees; the input is never copied wholesale to output. The implementation also enforces limits on input bytes, output bytes, node count, depth, attribute sizes and text sizes.

Deployment advice

For untrusted-user uploads, make the sanitized document only one defense layer:

  1. Keep the original SVG out of any publicly served directory.
  2. Serve the sanitized output with Content-Type: image/svg+xml, ideally from a separate cookieless domain with Content-Disposition: attachment for downloads. For display, prefer <img src="..."> rather than inline SVG.
  3. Use CSP restricting script-src 'none'; object-src 'none'; base-uri 'none'; default-src 'none'; img-src 'self' data: on the serving endpoint where appropriate, and ensure application CSP is also restrictive.
  4. Treat the output as untrusted in any subsequent XML/SVG manipulation pipeline; downstream software might add back risky features. If preserving more complex SVG features is necessary, consider rasterizing with a hardened, resource- isolated renderer instead of broadening the allowlist.
  5. Use browser-based regression tests, fuzz tests, and multiple-engine testing before relying on the package for production security.

No SVG sanitizer can promise protection against all future rendering-engine vulnerabilities. This prototype targets active-content injection and external resource-loading pathways, not malicious graphical impersonation or renderer memory-corruption bugs.

Python code is MIT licensed (see LICENSE); the Luxembourg artwork, derivatives, and comparison images are CC BY-SA 4.0 as documented with the corpus.

Metadata

Release files for svg-sanitize 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for svg-sanitize 0.2.0
File Size Uploaded Provenance
svg_sanitize-0.2.0.tar.gz 26.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for svg-sanitize 0.2.0
File Interpreter ABI Platform
svg_sanitize-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.2 kB

Release files / svg_sanitize-0.2.0.tar.gz

Download URL svg_sanitize-0.2.0.tar.gz
Size 26.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f18e06846667f72b2199485b9fc865102f2558e67cdc930b39e7a3f909e02532
BLAKE2b-256 checksum
How to use checksums
e88bcecaa11e3aa2d5016271bf96ea4ae876df04a72039624ace11b7b8b22920
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / svg_sanitize-0.2.0-py3-none-any.whl

Download URL svg_sanitize-0.2.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8ed90684a3fa3149a7c737b6bdda3f8e7507ca301fae3f2fa8bc38bce295e8be
BLAKE2b-256 checksum
How to use checksums
43a9de4bc35ec72c9bc593eb930fb1297052b169a25d35adda5415417b79e0f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Security for svg-sanitize 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for svg_sanitize-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page