feat: introduce local-first SVG workbench

This commit is contained in:
2026-08-02 16:31:49 +02:00
commit 39d9802daa
97 changed files with 20702 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# Security model
SVG is active content. Threats include script/event execution, navigation,
external fetches and tracking, CSS resource resolution, `foreignObject`, native
animation triggers, namespace confusion, entity/DOCTYPE processing, parser
differentials, oversized compressed/data content and algorithmic complexity.
## Source and projection
Canonical source is inert text and remains available even when unsafe. A
separate projection is built from a cloned semantic document. DOMPurify's SVG
profiles are followed by an application policy that:
- removes `script`, `foreignObject`, `animate`, `animateMotion`,
`animateTransform`, `set`, foreign namespaces and all `on*` attributes;
- disables links/navigation and executable or external URL attributes;
- allows only unique local fragment references and bounded base64 raster image
data (`png`, `jpeg`, `gif`, `webp`, `avif`);
- parses style declarations/stylesheets with css-tree and rejects `@import`,
unsafe legacy properties, parse failures, non-local URLs, `expression()` and
resource-producing functions such as `image-set()` and `paint()`;
- reports findings with source ranges wherever a semantic node/range exists.
DOCTYPE is excluded before DOM parsing; entity declarations invalidate the
semantic projection. Sanitized export removes temporary mapping attributes.
Sanitize is never applied to canonical source without preview and acceptance.
## Isolated preview
The projection is an iframe `srcdoc` with `sandbox="allow-scripts"` and no
`allow-same-origin`, giving it an opaque origin. Its child CSP is:
```text
default-src 'none'; connect-src 'none'; object-src 'none'; frame-src 'none';
base-uri 'none'; form-action 'none'; img-src data: blob:;
style-src 'unsafe-inline'; script-src <controller-origin>
```
The sole script is the packaged `canvas-frame-controller.js`. It is loaded with
anonymous CORS, accepts messages only from `parent` with a random channel,
validates message fields and removes its script element. The parent validates
both `event.source` and channel.
Because a sandbox without same-origin treats the script request as cross-origin,
serve this one app-relative asset with:
```text
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin
```
Keep `Cross-Origin-Resource-Policy: same-origin` for other files. Use an Nginx
`map`/header value, not a nested `location` that would drop inherited security
headers. Toolbox Portal 0.10.0 implements and tests this exception.
## Limits and tests
Hard limits are defined in `src/app/limits.ts`. SVGZ uses streaming
decompression and stops once output crosses the source limit. Workers are
revisioned, timed out and cancelable. Tree/diagnostic rendering is capped.
Project-authored tests cover script, handlers, links, external image/use/filter,
CSS imports/URLs/parser failures/resource functions, native animation,
namespaces, duplicate IDs/cycles, malformed entities/XML, depth/attribute/path
and data limits. Browser tests assert no script callback and no request to the
hostile domain.
## Limitations and reporting
This policy protects this application's projections and exports; it is not a
general guarantee for arbitrary downstream embedding. Browser SVG/CSS parsers
and DOMPurify remain dependency/security boundaries. Bounded data URLs may still
decode expensive images within the raster-pixel limits. CSP header regressions
can break the fixed controller even when the content remains inert.
Report vulnerabilities privately through the repository owner/contact before
opening a public issue when disclosure could expose users. Include the SVG,
browser, deployment headers and observed network/execution behavior without
sensitive user files.