# Risk map — sveltejs/svelte

> Canonical page: https://codevetter.com/unpack/svelte/risk-map

# Risk map — sveltejs/svelte

Scanned commit 636eaaaa6f064b55072e7d192bb76dc9d8c4516e (2026-09-18). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

The highest-severity surfaces are the two `innerHTML` sinks (`{@html}` and the template reconciler) and the server-side CSP/hash path, all of which are correct-by-design but unforgiving of misuse. Structurally, the biggest fragility is module-global mutable state on both sides of the codebase — `compiler/state.js` makes `compile()` non-reentrant, and `active_reaction`/`active_effect`/`current_batch`/`old_values` make the runtime a single-threaded global machine where any missed `finally` leaks state across components. Blast radius is concentrated in `runtime.js`, `reactivity/batch.js`, and `phases/2-analyze/index.js`, each of which sits under thousands of fixtures. Legacy code is not dead but is a permanent second code path: legacy mode, legacy AST conversion, and `beforeUpdate`/`afterUpdate` all have their own branches.
- `{@html}` is the primary XSS surface and is documented as such: `html()` assigns user values straight to `parent_node.innerHTML` or `wrapper.innerHTML` with no sanitisation, relying only on the fact that `innerHTML` does not execute `<script>`. — [packages/svelte/src/internal/client/dom/blocks/html.js#L78-145](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/dom/blocks/html.js#L78-L145)
- A second `innerHTML` sink exists in the template reconciler, where compiled template strings are assigned via `create_trusted_html(html.replaceAll('<!>', '<!---->'))` — safe only because the input is compiler-generated rather than user-supplied. (inferred) — [packages/svelte/src/internal/client/dom/reconciler.js#L20-26](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/dom/reconciler.js#L20-L26)
- `escape_html` escapes only `&`, `<`, and (in attribute mode) `"` — single quotes and `>` are deliberately not escaped, so any compiler codegen path that emits a single-quoted attribute without also escaping would be an injection hole. (inferred) — [packages/svelte/src/escaping.js](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/escaping.js)
- The server-side CSP path computes `sha256` hashes over inline script bodies and pushes them onto `global.csp.script_hashes`; `invalid_csp` is thrown only for the hash+nonce combination, and the nonce is interpolated into the attribute unescaped (` nonce="${this.global.csp.nonce}"`). — [packages/svelte/src/internal/server/renderer.js#L960-980](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/server/renderer.js#L960-L980) [packages/svelte/src/internal/server/index.js#L71-77](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/server/index.js#L71-L77)
- `internal/server/crypto.js` performs a deliberately obfuscated dynamic import — `import(/* @vite-ignore */ module_name)` on the string `'node:crypto'` — specifically so static analysers cannot see the Node dependency; this defeats bundler and supply-chain tooling that relies on static import graphs. — [packages/svelte/src/internal/server/crypto.js#L1-22](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/server/crypto.js#L1-L22)
- The compiler is not reentrant: `compiler/state.js` holds `warnings`, `source`, `filename`, `dev`, `runes`, `ignore_stack`, and `ignore_map` as module-level `let`s that `reset()` clobbers, so two concurrent `compile()` calls in one process would interleave diagnostics. (inferred) — [packages/svelte/src/compiler/state.js#L10-55](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/compiler/state.js#L10-L55) [packages/svelte/src/compiler/state.js#L130-150](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/compiler/state.js#L130-L150)
- The runtime has the same shape of risk at larger scale: `active_reaction`, `active_effect`, `untracking`, `new_deps`, `skipped_deps`, `untracked_writes`, `write_version`, and `update_version` are all module-level mutable globals in `runtime.js`. — [packages/svelte/src/internal/client/runtime.js#L60-150](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/runtime.js#L60-L150)
- `Batch.flush()` relies entirely on its `finally` block to reset seven pieces of cross-cutting state (`flush_count`, `last_scheduled_effect`, `collected_effects`, `legacy_updates`, `current_batch`, `batch_values`, `old_values`); an early return added above that block would leave the whole runtime wedged. (inferred) — [packages/svelte/src/internal/client/reactivity/batch.js#L668-698](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/reactivity/batch.js#L668-L698)
- Signal semantics are encoded in 26 overlapping bitflags, and two of them collide by design — `MANAGED_EFFECT` and `DESTROYING` both use `1 << 24`, and `EFFECT_OFFSCREEN` and `DESTROYING` both use `1 << 25` — meaning correctness depends on flags never co-occurring on the same signal. — [packages/svelte/src/internal/client/constants.js#L1-56](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/constants.js#L1-L56)
- Hydration failure is silently recoverable by default: any non-Svelte error triggers `console.warn` + full client re-render, so a genuine SSR/CSR divergence only surfaces as a warning unless the consumer passes `recover: false`. — [packages/svelte/src/internal/client/render.js#L118-145](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/render.js#L118-L145)
- Legacy Svelte 4 support is a permanently parallel code path, not dead code: `legacy_mode_flag`, `compiler/legacy.js`'s AST `convert`, `legacy_pre_effect`/`legacy_pre_effect_reset`, `internal/client/dev/legacy.js`, and the `beforeUpdate`/`afterUpdate` callback queue all exist solely for it. — [packages/svelte/src/internal/flags/index.js#L1-10](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/flags/index.js#L1-L10) [packages/svelte/src/compiler/legacy.js#L1-20](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/compiler/legacy.js#L1-L20) [packages/svelte/src/index-client.js#L186-230](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/index-client.js#L186-L230) [packages/svelte/src/internal/client/reactivity/effects.js#L311-370](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/client/reactivity/effects.js#L311-L370)
- Async mode is experimental and dual-pathed — `enable_async_mode_flag` is set only by importing `svelte/internal/flags/async`, and there is a `disable_async_mode_flag` explicitly annotated 'ONLY USE THIS DURING TESTING' — so the non-async path is only covered by the single `TestNoAsync` CI job. — [packages/svelte/src/internal/flags/index.js#L6-18](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/flags/index.js#L6-L18) [packages/svelte/src/internal/flags/async.js](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/packages/svelte/src/internal/flags/async.js) [.github/workflows/ci.yml#L46-62](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/.github/workflows/ci.yml#L46-L62)
- The `autofix.yml` workflow runs on `issue_comment` with `contents: write` and pushes a commit to the PR head branch; it mitigates the obvious fork risk by refusing non-same-repo PRs and gating on `OWNER`/`MEMBER`/`COLLABORATOR` author association. — [.github/workflows/autofix.yml#L1-70](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/.github/workflows/autofix.yml#L1-L70)
- `ecosystem-ci-trigger.yml` mints a GitHub App token from `ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY` and includes an explicit TOCTOU guard: it fails with a 'Security warning' if `pr.head.repo.pushed_at` is newer than the triggering comment's `created_at`. — [.github/workflows/ecosystem-ci-trigger.yml#L60-105](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/.github/workflows/ecosystem-ci-trigger.yml#L60-L105)
- Four ESLint rules are disabled repo-wide with a `TODO: enable these rules` comment — `@stylistic/quotes`, `@typescript-eslint/no-unused-vars`, and `prefer-const` — so unused variables and accidental `let` are currently unenforced across all of `src/`. — [eslint.config.js#L57-66](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/eslint.config.js#L57-L66)
- `playgrounds/sandbox/scripts/download.js` combines `execSync` from `node:child_process`, `new URL(url_arg)` on user input, and a Playwright Chromium launch against arbitrary URLs — the only shell-out + arbitrary-network path in the repo, and it is unlinted (`playgrounds/sandbox/**` is in the ESLint ignore list). — [playgrounds/sandbox/scripts/download.js#L1-55](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/playgrounds/sandbox/scripts/download.js#L1-L55) [eslint.config.js#L88-104](https://github.com/sveltejs/svelte/blob/636eaaaa6f064b55072e7d192bb76dc9d8c4516e/eslint.config.js#L88-L104)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
