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+; the only runtime dependency is defusedxml.
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))
CLI
svg-sanitize examples/malicious.svg -o clean.svg
# alternatively:
python -m svg_sanitize examples/malicious.svg -o clean.svg
Run tests:
python -m pytest -q
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.
Before the first release:
- Create a GitHub Actions environment named
pypiin this repository. Add any desired deployment protection rules there. - Configure a PyPI trusted publisher
for the
svg-sanitizeproject with owneradulau, repositorysvg-sanitize, workflow filenameci.yml, and environment namepypi. 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.
What is allowed
Only 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. This is an explicit security/functionality tradeoff.
What is forbidden
- JavaScript and scriptable elements;
onload,onclick, and all event handlers. - CSS (
<style>,style=), including@import,url(...), and expressions. - All links and resources, including
href,xlink:href, external and internal references,data:URLs,<image>,<use>, fonts, filters and gradients. <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
id,class,xml:base, andxmlns:xlink).
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:
- Keep the original SVG out of any publicly served directory.
- Serve the sanitized output with
Content-Type: image/svg+xml, ideally from a separate cookieless domain withContent-Disposition: attachmentfor downloads. For display, prefer<img src="...">rather than inline SVG. - 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. - 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.
- 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.
MIT licensed (see LICENSE).
Metadata
Release files for svg-sanitize 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| svg_sanitize-0.1.0.tar.gz | 16.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| svg_sanitize-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.4 kB
Release files / svg_sanitize-0.1.0.tar.gz
| Download URL | svg_sanitize-0.1.0.tar.gz |
|---|---|
| Size | 16.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3e5f0e2118e6d57e15ba41a6184acb17746997976d06a415dce2d6d1d4dbe8e4
|
|
BLAKE2b-256 checksum How to use checksums |
257f8c139338220cd9b0372b38c3ea70fcac550f215816bc81dbe54927d10a14
|
| 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 24, 2026.
Transparency logRelease files / svg_sanitize-0.1.0-py3-none-any.whl
| Download URL | svg_sanitize-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
75aaf8f89d57fcb9ad6129f69624d0142c94b8b3180ff9a356087778648ce2c0
|
|
BLAKE2b-256 checksum How to use checksums |
0d4036666926c0e81ce3a0b057cb5f9b09a49d5ec0ef5c3fadee9eb7dd66ae09
|
| 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 24, 2026.
Transparency logSecurity for svg-sanitize 0.1.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.
Attestation for svg_sanitize-0.1.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 24, 2026.
Transparency logA 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.