Skip to the contentSkip to the navigation
RootsGitHubMenu
On this page

Security hardening: what was fixed

Worktree: the security worktree (nothing committed; VERSION and CHANGELOG.md untouched). git diff sec-base shows everything: 136 tracked files changed, plus the new files src/build/tests/security.lisp, src/build/targets/tests/ (3 files), src/pm/tests/hardening.lisp and shells/android/templates/data_extraction_rules.xml.

Result #

  • Every critical, high and medium finding is fixed by default, except PM-06, which is closed only by two opt-in switches (reason below).
  • Full suite in the worktree: 472 tests, 471 passed; the one failure is e2e-extension-popup (known, environmental). Log: sec/full-suite.log.
  • The full-suite script only loads systems named …/tests, so two systems were run separately afterwards:
    • roots-pm-tests: 64 of 64 passed (one run; see "SBCL abort" under Open).
    • roots-data-tests: 38 of 39 passed. The failure is data-e2e-notes-extension, the same ERR_BLOCKED_BY_CLIENT as e2e-extension-popup.
  • The nine browser tests of roots/data that were PENDING for every owner now ran: I put the real @sqlite.org/sqlite-wasm 3.54.1-build1 (hashes equal to the pins) into examples/notes/node_modules/ of the worktree (git-ignored; still there). They found one real bug in the WASM worker, fixed below.
  • All examples build for all six targets (todo, notes, gallery, hello-lisp × web, pwa, ext, ios, android, term: 24 builds, exit 0) into integ/ex/. browser-counter and hello.lisp have no roots.lisp and were not built.
  • Android Kotlin (shell core, SqlitePlugin.kt, StoragePlugin.kt, the four test classes) compiles against android-36 with Android Studio's kotlinc; JUnit: 34 tests OK. This covers the data owner's and re-tester's Kotlin, which had never been compiled.
  • Not verified anywhere: behaviour on an Android device or in a WKWebView (gesture rule, frame reply, backup flags), Gradle and Xcode builds, Linux, browsers other than Chromium.

What the integrator changed #

Applied from "needsFromOthers" and "stillOpen":

ChangeFilesTest
roots.lisp keys :csp, :pwa :precache-exclude, :native :backup; :native :plugins defaults to the derived list. This activates Trusted Types, backups off and the derived plugin list, which were dormantsrc/build/config.lispbuild-config-security-keys, the targets-sec-* tests (now without their schema shims taking effect)
Trusted Types also for builds with roots/data: the worker URL goes through a policy roots-data that passes only that URL; the CSP names it. The bundler finds a worker URL handed through one callruntime/data/wasm.lisp, src/build/targets/dom.lisp, src/bundle/module.lispdata-e2e-notes-web (whole flow in Chromium under the policy), data-build-every-target, bundle-emit-worker-url-through-a-call, targets-sec-csp-trusted-types
WASM worker: a cached statement whose last run failed threw that old error on its next run and did not execute (found by the DATA-16 conformance test, first run here)runtime/data/worker.lispdata-runtime-conformance-wasm-chromium
Dev error page written through write-output-file (DEV-5); dev server header carries the :csp sourcessrc/build/devserver.lispdevserver-error-page-is-not-written-through-a-link, devserver-header-carries-the-csp-sources-of-roots-lisp
migrations/ as a symbolic link is B163 (rest of BB-05)src/build/data.lispbuild-migrations-are-not-read-through-a-link
<!-- and a leading --> in module code are C111 (BB-13)src/parse/lexer.lispparse-lexer-html-comments-in-modules
Unbundled builds (roots dev, --no-bundle) check the project's own modules for redeclarations when they are compiled (BB-15)src/build/graph.lispbuild-unbundled-modules-are-checked-for-redeclarations
libsqlite3 is loaded by absolute path on macOS; an empty or relative element in the library path variable is D170 (PM-01 pattern in roots db)src/data/sqlite.lispdata-binding-library-is-not-searched-in-the-working-directory
roots/data's native driver checks the bridge version like roots/platform (NE-9)runtime/data/native.lispconformance over the Swift plugin still passes; no test of a wrong version
h(): a handler key inside $attrs becomes a listener instead of an R304 throwruntime/ui/h.lispUI suite passes; no new test
Launchers drop a relative XDG_DATA_HOME / XDG_CONFIG_HOME / XDG_CACHE_HOME (ASDF died on one)bin/roots, bin/test-systemchecked by hand
roots build prints the derived plugin list (the summary was given a fresh build object)src/build/commands.lispchecked by hand
*compile-time-lisp* exported from roots.js; plugin names take ASCII digits onlysrc/package.lisp, src/build/config.lispexisting tests
Target security tests are part of roots-build/tests (the separate system file is gone)src/build/roots-build.asdthey run in the full suite
Tests updated for the new defaultssrc/data/tests/build.lisp, src/data/tests/e2e.lisp, src/build/tests/native.lisp, src/build/targets/tests/security.lisp
Docs: diagnostic rows, roots.lisp reference, security modeldocs/build.md, docs/data.md, docs/packages.md, docs/bundling.md, docs/parser.md, docs/native.md, docs/targets.md, README.md

Hot paths: nothing was added to runtime/core, runtime/dom or runtime/reactivity by the integration. The worker change is one try per statement run; the roots-data policy is created once per page.

Decisions taken as secure defaults #

These change behaviour for existing apps. Each has a documented way back.

  • Trusted Types on for web, pwa and the Roots shells. An app that ships a library which assigns innerHTML sets :csp (:trusted-types nil).
  • Native backups off. :native (:backup t) turns them on.
  • Native plugin allow-list derived from imports. An app that calls RootsNative.call(…) itself lists :native (:plugins (…)); otherwise it gets BRIDGE_UNKNOWN_PLUGIN.
  • A package's Lisp is refused (B129), no opt-in.
  • Another SQLite WASM version is an error (D164) unless pinned in roots.lisp.

Decisions for you #

  • PM-06 defaults. Whether :builds (approvals need a line in roots.lisp) and :verify-age should be on by default. Left opt-in: the first breaks CI for every project with approved install scripts until it adds the key, the second adds a registry request per package to every install.
  • import {} from "pkg" now counts as a side-effect import and keeps the module (the re-tester reversed the owner's test).
  • R303 / R304 / R306 throw into the error handler instead of warn-and-drop.
  • iOS file protection for the database is left at the platform default (see NE-3).

Findings #

Status: fixed, mitigated (closed in part or by an option), documented, open.

Build, bundler, parser #

IdSeverityStatusTest
BB-01 / DEV-1criticalfixed: package Lisp is B129, compile-time forms off for package units, B125 for imports into node_modules by pathbuild-package-lisp-is-refused, build-compile-time-lisp-gate, build-app-cannot-reach-into-node-modules-by-path
BB-02highfixed, including the same bug in the bundler's renaming and the catch-body collision the re-tester foundbundle-minify-var-declared-in-a-block, bundle-minify-catch-body-avoids-the-parameter, bundle-minify-output-check, bundle-emit-names-over-declarators-in-blocks
BB-03 / DEV-5mediumfixed: output writes and deletes never go through a link, also --out by another spelling, hard links, and the dev error pagebuild-output-refuses-symlinked-directories, build-output-link-check-knows-the-project-by-another-spelling, build-output-replaces-a-hard-linked-file, devserver-error-page-is-not-written-through-a-link
BB-04mediumfixed: the two files ship only with the pinned SHA-256. Residual: they are hashed and then read again, so someone rewriting node_modules during a build could swap thembuild-sqlite-wasm-files-are-pinned, build-sqlite-wasm-other-version-needs-a-pin, data-build-every-target (real files)
BB-05lowfixed, migrations/ includedtargets-sec-public-symlinked-directory-is-refused, targets-sec-native-sources-are-not-read-through-links, build-migrations-are-not-read-through-a-link
BB-06lowfixed (B125)build-package-directory-outside-the-project
BB-07lowfixed for imports; a bare call to a package function annotated @__NO_SIDE_EFFECTS__ is still dropped without a diagnostic (documented as by design)build-keeps-a-package-imported-for-its-effects, build-bare-import-detection
BB-08lowfixedbuild-javascript-syntax-error-is-a-build-error, build-unexpected-conditions-are-coded
BB-09lowmitigated: stack exhaustion is C109 / B192, five quadratic passes removed. Still slow, not crashing: passes in src/template and the Lisp compiler, two small list scans in emit.lisp. B192 does not name the filebundle-model-deep-and-long-input, bundle-minify-scales, build-deeply-nested-input-is-a-coded-error, build-template-classes-scan-is-linear
BB-10hardeningfixed at the source (B128, B140); the marker stays one path per linebuild-package-manifest-is-validated, build-emit-refuses-unsafe-paths
BB-11hardeningmitigated: cache and the binary's extracted toolchain must be private (B167), ROOTS_NO_CACHE=1. Entries are not authenticated, so a poisoned cache restored into a private directory is still usedbuild-disk-cache-must-be-private, build-binary-toolchain-directory-must-be-private
BB-12 / DEV-6hardeningfixed; the test is a static check, the odd-path run was by handbuild-launchers-load-only-the-toolchains-systems
BB-13hardeningfixed: documented, and <!-- / --> are C111parse-lexer-html-comments-in-modules
BB-14(verifier)fixed; a net for mistakes, not a secret scannertargets-sec-public-key-material-is-never-published
BB-15(verifier)fixed (C110), bundled and unbundled. Not reported: (function f(){ let f; let f; })parse-scope-redeclaration-error, bundle-model-redeclarations, build-unbundled-modules-are-checked-for-redeclarations
BB-16(verifier)not needed with hash pinning
DEV-9hardeningdocumented (docs/build.md "What a build trusts", README). No --no-compile-time-lisp mode

Dev server #

IdSeverityStatusTest
DEV-2medium with --hostfixed. Residual by design: a local process can keep taking the 64 slots in 5 s turnsdevserver-slow-request-head, devserver-unread-response, devserver-remote-peers-leave-room
DEV-3lowfixed (32 channels). Residual: a local process can hold all 32; a tab that got 503 does not reconnect until reloadeddevserver-event-clients-are-limited
DEV-4lowfixeddevserver-stalled-event-client
DEV-7hardeningfixed; non-HTML files are 'self' / SAMEORIGIN, not 'none'devserver-cross-site-frames-refused
DEV-8hardeningfixed as far as a cookie name can: the browser still sends the cookie to other ports of the addressdevserver-token-for-remote-peers, devserver-host-needs-the-port
NEW-1(verifier)fixeddevserver-events-only-for-the-page
NEW-2(verifier)fixed for the dev server. roots dev --target term handles build errors only (all build failures are build errors now)devserver-watcher-survives-internal-errors

Package manager #

IdSeverityStatusTest
PM-01highfixed; the same pattern in roots db fixed by the integratorpm-libraries-load-from-absolute-paths, pm-libraries-not-taken-from-the-working-directory, data-binding-library-is-not-searched-in-the-working-directory
PM-02highfixed (PM152)pm-linker-refuses-symlinked-layout
PM-03highfixed (PM138, PM140, PM172)pm-dependency-names-are-validated, pm-lockfile-names-are-validated
PM-04lowfixed, including Unicode case twins of package.jsonpm-tar-one-file-per-path, pm-install-checks-the-manifest-that-is-on-disk
PM-05mediumfixed (PM142)pm-lockfile-exotic-resolved-is-tied-to-its-source, pm-lockfile-cannot-redirect-a-file-dependency
PM-06mediummitigated, opt-in only. By default a change to roots.lock alone can still pin a version inside the cooldown and approve its install script. Closed with :packages (:builds (…)) and :verify-age t / --verify-agepm-lockfile-alone-cannot-approve-or-skip-the-cooldown
PM-07lowmitigated: an index is validated when read; one that lists other valid store files is not detected (documented)pm-store-index-is-read-as-untrusted
PM-08lowfixedpm-gunzip-does-not-trust-the-trailer
PM-09lowfixedpm-http-authorization-stays-on-its-origin
PM-10lowfixedpm-http-request-line-cannot-be-extended
PM-11lowfixed as the audit proposed: a project .npmrc can still send a variable as a credential to a host it names, with warning PM171pm-npmrc-project-file-is-not-trusted-with-the-environment
PM-12lowfixedpm-loopback-is-an-address-not-a-prefix
PM-13hardeningmostly fixed. Open: sha1-only integrities are accepted without an upgrade to sha512; an optional package can still be marked as for another platform when its tarball is not in the storepm-install-verify-rebuilds-node-modules, pm-lockfile-cannot-drop-a-package-as-foreign-platform, pm-tls-body-must-end-with-a-tls-close, pm-proxy-must-be-plain-http, pm-package-text-cannot-repaint-the-terminal
PM-14(verifier)fixed (PM153)pm-names-differing-in-case-are-refused
PM-15(verifier)fixed; the cache now lives inside the 0700 v1/pm-metadata-cache-is-per-registry

Templates and runtime #

IdSeverityStatusTest
XSS-1mediumfixed: names judged in lower case, once per namedom-test.lisp: "attribute names in any case…", "URL attributes in any case…", "names from data through fallthrough attributes…"
XSS-2 / V-3lowfixed (C211)tpl-security-attribute-names-any-case
XSS-3lowfixed, including the backslash bypass (x:/\host)router-test.lisp "resolve: a target that is no in-app path…"; dom-test.lisp router history test, R407 test
XSS-4lowfixedrouter-test.lisp "resolve: query and params have no prototype…"
XSS-5hardeningfixedtpl-security-spread-own-keys
XSS-6 / V-1lowfixed (C208, R306)tpl-security-forbidden-elements; core and DOM tests for :is
XSS-7lowfixed (C224, R307 for data: frame documents)tpl-security-static-urls; dom-test.lisp "data: URLs are refused where they load a document…"
XSS-8hardeningfixed for web, pwa, Roots shells and roots/data builds; :csp sources validated. Off for the extension popup and Capacitor. frame-ancestors and SRI limits documentedtargets-sec-csp-trusted-types, targets-sec-csp-sources-from-roots-lisp, targets-sec-csp-trusted-types-in-chromium, targets-sec-csp-examples-run-under-trusted-types, data-e2e-notes-web
XSS-9hardeningdocumented (docs/templates.md §7, docs/runtime.md §6)
V-2(verifier)fixed: invalid names are dropped with R308. R303 / R304 still throw (decision above)dom-test.lisp R308 tests

Targets and native shells #

IdSeverityStatusTest
NE-1mediumfixed: every precached file is fetched past the HTTP cache with its SHA-256; a failed install keeps the running version and its cachetargets-sec-pwa-update-is-not-filled-from-the-http-cache, targets-sec-pwa-tampered-file-fails-the-install, targets-sec-pwa-failed-reinstall-keeps-the-running-cache
NE-M1(verifier)fixedtargets-sec-pwa-missing-file-is-reported, targets-sec-pwa-precache-exclude
NE-2lowfixed on iOS (authorizer, run over the Swift plugin); Android refuses by the statement's first words: compiled and unit-tested, not run on a deviceconformance "native bridge: the sqlite plugin keeps a page inside its own database file"; SqlStatementsTest
NE-3lowfixed, now active: backups off unless :native (:backup t). Limits: iOS UserDefaults of the storage plugin cannot be excluded; the iOS file-protection class is the platform defaulttargets-sec-native-backups-are-off-unless-asked-for, targets-sec-native-backup-rules-pass-aapt2
NE-4refutedno fix; docs/native.md no longer claims more than the Android code does
NE-5hardeningfixed, now activetargets-sec-native-default-plugins-are-what-the-app-uses, …-include-sqlite-for-roots-data, native-android-project
NE-6hardeningfixed (B105)targets-sec-native-plugin-names-map-to-one-class
NE-7lowfixedtargets-sec-pwa-two-apps-on-one-origin-keep-their-caches
NE-8hardeningfixedtargets-sec-native-bridge-is-absent-in-frames, BridgeTest
NE-9hardeningfixed, roots/data's accessor includedtargets-sec-native-adapter-checks-the-bridge-version, BridgeTest, Swift checks
NE-10hardeningmitigated: npm install --ignore-scripts; the app's own Gradle verification metadata is carried over, none is shippedtargets-sec-capacitor-install-runs-no-scripts, targets-sec-native-gradle-verification-metadata-is-carried-over
NE-11hardeningfixed in code; Android part compiled onlySwift Shell.mayLeave checks
NE-12, NE-13refutedno fix
NE-14hardeningfixedJsonTest.unicodeEscapesAreFourHexDigits
NE-15hardeningfixed; UIKit code not compiled herenone
NE-16hardeningfixedBridgeTest, ShellTest, Swift checks
NE-17hardeningfixed (warning B158)targets-sec-ext-broad-permissions-are-reported
NE-18hardeningdocumented; INTERNET stays (dropping it cannot be checked without a device)

Data #

IdSeverityStatusTest
DATA-01mediumfixed: toolchain connections cannot ATTACH; migrations with transaction control are D136, also behind a control characterdata-migrations-cannot-write-other-files, data-migrations-cannot-end-their-transaction, data-binding-stays-in-its-file
DATA-02lowfixedconformance "live: INSTEAD OF triggers, RAISE(IGNORE) and temp triggers do not hide a write"
DATA-03lowfixed: integers beyond 2^53 are decimal strings. Still rounded: lastInsertRowid, Capacitorconformance "integers beyond 2^53 are read as decimal strings…"
DATA-04lowfixeddata-db-types-hostile-names
DATA-05lowfixed, migration error messages includeddata-db-query-output-is-safe-to-print, data-migration-errors-are-safe-to-print
DATA-06lowfixed; the launcher part by the integratornode-suite mode tests, data-db-files-are-private
DATA-07lowfixed (<name>.sqlite3, old file adopted)conformance native-bridge test, over the Swift plugin too
DATA-08lowfixedconformance "text with a NUL character is stored whole"
DATA-09lowfixed in roots/data (D112, D113, D114, D111 for NUL). The Android plugin called directly still binds a missing value as NULLconformance "parameters: NaN and Infinity, a wrong count and a second statement are refused"
DATA-10lowfixed (D136); Bun scripts stop at the first failing statementconformance "migrate: a migration that ends its transaction…", "a script stops at the statement that fails…"
DATA-11hardeningdocumented
DATA-12hardeningdocumented; no row limit
DATA-13hardeningmitigated: trusted_schema off, ATTACH refused (D117). Defensive mode on node:sqlite onlyconformance "connections: the schema is not trusted, and ATTACH is refused unless asked for"
DATA-14hardeningfixedconformance "a column named proto is an ordinary column"
DATA-15hardeningfixed and now run in Chromium. Since the DX round a second tab shares the first tab's database (same origin, one connection held by the tab that owns the Web Lock); a tab that cannot share (no Web Locks or BroadcastChannel, or next to a tab of an older build) still gets an in-memory database unless { memoryFallback: false }data-runtime-conformance-wasm-chromium, data-e2e-wasm-integrity
DATA-16(verifier)fixedconformance "live: a write that fails is still reported…" (all drivers, WASM included)
DATA-17(verifier)fixed (D115)conformance "table(): an undefined where value is refused…"
DATA-18(verifier)fixed; data-runtime-conformance-bun passes on this Mac nowdata-runtime-conformance-bun

Open #

  • PM-06 (medium): opt-in only, see above.
  • SBCL abort in the pm suite. The re-tester saw SIGABRT (pointer being freed was not allocated, in the cleanup of sb-posix:stat/lstat) in 4 of 15 runs under load; the pm owner's GC call in one test does not cover it. Root cause unknown; whether a real roots install can hit it is unproven. My one run passed. Next step: a newer SBCL, or a breakpoint on malloc_error_break.
  • Device checks for the Android and iOS shell changes (NE-3, NE-8, NE-11, NE-15) and the Android sqlite rules (NE-2).
  • New, from the data re-test, documented only: a tx statement written INSERT OR ROLLBACK (or RAISE(ROLLBACK)) ends the transaction; a callback that catches the error and goes on writes in autocommit.
  • Hardening left as it is: BB-07 annotation warning, BB-09 slow passes, BB-11 authenticated cache entries, PM-07 index authentication, PM-13 sha1 upgrade, DATA-12 row limit, DATA-13 defensive mode on other drivers, Trusted Types for the extension and Capacitor, NE-10 shipped Gradle metadata, NE-18.
  • Small: write-json in src/build/util.lisp prints doubles with ~f (the data command no longer uses it); docs/runtime.md says R304 covers every name starting with on, the code exempts the bare name on; a static on-click= attribute compiles and throws R304 at run time.

Behaviour changes for app authors #

Build and config

  • New roots.lisp keys: :csp, :pwa (:precache-exclude …), :native (:backup …), :data (:sqlite-version … :sqlite-mjs-sha256 … :sqlite-wasm-sha256 …), :packages (:builds … :verify-age …).
  • Pages of web, pwa and the Roots shells require Trusted Types (:csp (:trusted-types nil) turns it off). Extra origins for fetch, images, fonts, media and frames go in :csp.
  • A dependency that is Lisp is B129. Importing into node_modules by path, or a node_modules/<name> linking outside the project (npm link), is B125. A package without a valid package.json name and version is B128.
  • A name declared twice in a module is C110, in bundled and unbundled builds. <!-- in module code is C111.
  • import "pkg" and import {} from "pkg" in app code keep the module despite "sideEffects": false.
  • Minified output has other short names where a var sits in a block or catch clause. catch (e) { var e } keeps its name; a clash is B137.
  • Symbolic links: inside the output directory B141; public/, icon.png, migrations/, native/<platform>/ as links B163. More key and secret files are skipped from public/ (B164).
  • roots/data on DOM targets: another @sqlite.org/sqlite-wasm version is an error (D164) unless pinned; changed files are D166.
  • Errors are one coded line (B191, B192, src/x.js:1:23: C102 …); ROOTS_DEBUG=1 shows the backtrace. ROOTS_NO_CACHE=1 turns the compile cache off; a cache directory others can write is not used (B167).
  • bin/roots and bin/test-system load only roots.asd, src/*/*.asd, runtime/*/*.asd.

Dev server

  • Host needs the port. Other sites cannot frame or embed dev-server files. The reload channel is for the page only, at most 32 at once. Time limits on request heads (5 s) and stalled responses. The --host token cookie is roots_token_<port>. Internal errors during a rebuild show in the overlay and roots dev keeps running.

Package manager

  • A symlinked node_modules (or .roots, .bin, a package directory) is PM152. Invalid dependency names are PM138 / PM172 / PM140. Names differing only in case are PM153.
  • Project .npmrc: ${VAR} in a registry URL is PM170, in a credential it warns (PM171); credential keys match host, port and path segments exactly. Authorization is dropped on a redirect to another port or scheme. An https:// proxy is refused (PM107).
  • Unpacked size per package is 512 MB (was 2 GB). Stricter roots.lock checks (PM142, PM146, PM137). Plain http only to localhost and 127.x.x.x.
  • macOS: OpenSSL only from Homebrew, MacPorts or an absolute $ROOTS_OPENSSL_DIR. A library path variable with an empty or relative element is refused (PM101; D170 for roots db). On Linux a leading or trailing colon in LD_LIBRARY_PATH is such an element.
  • New: roots install --verify, --verify-age; roots doctor prints the library files.

Templates and runtime

  • Attribute names are judged in any case, also when they come from data: onClick strings throw R304, srcDoc / InnerHTML throw R303, javascript: under HREF is dropped (R307). data: is refused as the document of iframe, frame, embed and object.
  • <component :is="string"> refuses script, style, iframe, frame, frameset, object, embed, base, meta and link (R306) unless a component of that name is registered.
  • :srcDoc, :src-doc, :InnerHTML are C211; static URLs with a tab or newline in the scheme and <object data="javascript:…"> are C224; <style> under v-pre is C208.
  • v-bind="obj" binds own keys only. Invalid attribute names are dropped with R308.
  • route.params and route.query have no prototype. Router targets never leave the app; a refused pushState resolves false (R407).

PWA and native

  • The service worker install fails if a precached file is missing or not byte-identical to the build (a host that rewrites files stops updates); updates.onError(message) reports it. Cache names changed, so users download the precache once more. Host files such as _headers and CNAME are not precached.
  • Native: plugins derived from imports, backups off, a bridge of another ABI version or a frame gets BRIDGE_UNAVAILABLE, script cannot open the browser without a user gesture (use openUrl), iOS link previews off, the database file is <name>.sqlite3 (an old <name> is renamed on first open). Extension builds warn about broad permissions (B158).

roots/data

  • D113 (value count), D114 (second statement), D112 (NaN, Infinity), D111 (NUL in SQL), D117 (ATTACH, DETACH, VACUUM INTO without { attach: true }), D115 (undefined in where), D136 (transaction control in a migration), D154 ({ memoryFallback: false }).
  • trusted_schema is off ({ trustedSchema: true } turns it on). Integers beyond 2^53 come back as decimal strings. A __proto__ column is an ordinary property. Live queries also fire for failed writes and trigger writes. On Bun, scripts stop at the first failing statement.
  • roots db query escapes control characters; existing database files are tightened to 0600; migrate and types cannot ATTACH.

New diagnostic codes #

FamilyNewMeaning changed
BuildB129, B137, B158, B167, B191, B192B125, B128, B140, B141, B163 (also an error), B164
ParserC110, C111C109
TemplatesC208, C211, C224
RuntimeR308, R407R303, R304, R306, R307
PackagesPM138, PM153, PM163PM101, PM105, PM106, PM107, PM112, PM121, PM122, PM131, PM137, PM140, PM142, PM146, PM150, PM152, PM170, PM171, PM172
DataD113, D114, D117, D136, D154, D166D111, D112, D115, D152, D164 (warning to error), D170

Environment variables: ROOTS_NO_CACHE, ROOTS_DEBUG, ROOTS_OPENSSL_DIR.

Draft CHANGELOG section #

## security hardening

A security audit of the whole tree (dev server, package manager, templates and runtime,
native shells and targets, build and bundler, data), every finding checked by a second
reviewer, then fixed and re-tested. After verification: one critical, four high and eight
medium findings; all are fixed by default except one (roots.lock approvals, below). Each fix has a regression
test. Several defaults change; each has a documented way back.

- **A package's Lisp no longer runs in the toolchain.** A dependency that resolved to a
  `.lisp` file, or a `.vue` with `<script lang="lisp">`, was compiled like app code, and
  compiling Lisp runs its macros: any package could run Common Lisp as the developer on
  every `roots build` and `roots dev`. It is now B129, with no opt-in; while a package file
  is compiled the compiler also refuses `(lisp …)`, `defmacro` and `macrolet`. Importing
  into `node_modules` by path, and a package directory that links outside the project, are
  B125. docs/build.md, "What a build trusts", says what a build trusts (your sources:
  completely; packages: not at all).
- **Minifier: a `var` in a block or catch clause could get the short name of a binding
  around it**, which changed what the program did (the audit's example made an
  `isAllowed` check pass for a forged token). The scope model now records where hoisted variables are
  written; the bundler's own renaming had the same hole. A catch clause with a `let` /
  `const` in its body and an unused parameter minified to code that did not load. The
  minifier's output is now compared with its input scope by scope before it is written.
  Short names differ from 0.3.0 for such code; `catch (e) { var e }` keeps its name (B137
  when it cannot).
- **Output directory:** `roots build` wrote and deleted through symbolic links inside it,
  so a checkout could carry a link that made a build overwrite files elsewhere. Every write
  and delete now walks the path without following links (B141), also for the dev server's
  error page. `public/`, `icon.png`, `migrations/` and `native/<platform>/` are never read
  through a link (B163), and more key material is kept out of `public/` (B164).
- **Package manager:** OpenSSL, zlib and SQLite are loaded by absolute path on macOS (a
  library in the working directory used to be picked up by `roots why`); a symlinked
  `node_modules/.roots` no longer makes `roots install` delete the link's target (PM152);
  dependency names are validated everywhere (`"../../src": "npm:x"` deleted directories;
  PM138); a git, URL or file entry of roots.lock must resolve where package.json says
  (PM142). Also: no heap exhaustion from a 25-byte gzip, Authorization stays on its origin,
  the request line cannot be extended, project `.npmrc` files are not trusted with the
  environment, loopback is an address, names differing in case are refused (PM153).
  **Not closed by default:** a change to roots.lock alone can still approve an install
  script or pin a version inside the cooldown; `:packages (:builds (…))` and `:verify-age t`
  (or `roots install --verify-age`) close it for projects that set them.
- **SQLite WASM is pinned by hash.** roots/data shipped `index.mjs` and `sqlite3.wasm` from
  whatever `node_modules` was found above the project. The build now ships them only when
  their SHA-256 is the pinned one (D166); another version is an error (D164, was a warning)
  unless roots.lisp pins it under the new `:data` key.
- **PWA updates are checked.** The service worker fetched its files through the HTTP cache
  and never compared them with the build, so an update could be assembled from stale files.
  Every file is now fetched past the cache with its SHA-256; a failed install keeps the
  running version and reports through the new `updates.onError`. Two apps on one origin no
  longer delete each other's caches.
- **Templates and the DOM backend:** the attribute rules were case-sensitive, so `onClick`,
  `srcDoc` or `HREF` from data (a spread, fallthrough attributes, `<component :is>` props)
  got past them. Names are now judged in lower case, once per name (a Map lookup per write
  in place of a chain of string comparisons). `<component :is>` refuses strings naming
  document-level tags (R306), the router never renders a link out of the app, route params
  and query have no prototype, and invalid attribute names are dropped (R308).
- **Trusted Types.** Pages of web, pwa and the Roots shells are written with
  `require-trusted-types-for 'script'`; the only policies are the runtime's own for the
  service worker and the roots/data worker URL. `:csp` in roots.lisp adds origins to the
  policy (never to `script-src`) and `:csp (:trusted-types nil)` turns the requirement off.
- **Dev server:** bounded request heads, responses and connection slots; at most 32 reload
  channels, for the page only; no socket is written while the server lock is held; no
  framing by other origins; `Host` needs the port; internal errors during a rebuild no
  longer end `roots dev`.
- **roots/data:** `roots db types` and `migrate --check` could write arbitrary files through
  a migration (ATTACH); toolchain connections cannot attach now, and a migration with
  `BEGIN` / `COMMIT` / `ROLLBACK` is refused before anything runs (D136). At run time ATTACH
  needs `{ attach: true }` (D117), `trusted_schema` is off, a wrong parameter count (D113),
  a second statement (D114), NaN and NUL are refused on every driver, integers beyond 2^53
  come back as decimal strings, live queries see writes made by triggers and by failing
  statements, and `roots db query` cannot repaint the terminal. The WASM worker ran a
  statement's stale error instead of the statement after a failure; fixed.
- **Native shells:** the plugin allow-list is what the app imports (it was all five
  built-ins), app data is excluded from backups unless `:native (:backup t)`, the sqlite
  plugin refuses ATTACH and VACUUM INTO, the bridge checks its ABI version and answers
  frames with `BRIDGE_UNAVAILABLE`, script cannot leave the app without a user gesture, logs
  name scheme and host only. Android and iOS code was compiled and unit-tested; nothing ran
  on a device.
- **Parser and build errors:** a name declared twice is C110, `<!--` in a module C111, deep
  nesting C109 or B192, a `.js` syntax error one line with file and position, anything
  unexpected B191 (`ROOTS_DEBUG=1` for the backtrace). `import "pkg"` keeps the module in a
  bundle whatever the package's `sideEffects` says.
- **New codes:** B129, B137, B158, B167, B191, B192; C110, C111; R308, R407; PM138, PM153,
  PM163; D113, D114, D117, D136, D154, D166. **New roots.lisp keys:** `:csp`, `:data`,
  `:pwa (:precache-exclude)`, `:native (:backup)`, `:packages (:builds :verify-age)`.
  **Environment:** `ROOTS_NO_CACHE`, `ROOTS_DEBUG`, `ROOTS_OPENSSL_DIR`.
- **Tests:** 472 in the `…/tests` systems (471 pass here; `e2e-extension-popup` cannot load
  an extension on this machine), 64 in the package manager's, 39 in roots/data's (the
  extension e2e fails the same way). roots-build's tests now load roots-pm.
- **Open:** the roots.lock default above; an intermittent SBCL abort in the package
  manager's test suite under load, cause unknown; device runs of the shell changes; no
  row limit in roots/data; cache entries and the store index are not authenticated.