# Security and privacy

Patterns, flags, replacement templates, subjects and imported projects are
untrusted.

- Syntax and execution use separate workers.
- Regex execution never occurs on the UI thread.
- Timeout, crash and cancellation terminate the worker.
- A terminated worker is never reused.
- Pattern, subject, replacement/list template, match, capture and output sizes
  are bounded.
- PCRE2 adds native match-step, depth and heap caps. Its C ABI returns only
  caller-owned bounded records and releases code, match data and match context
  on every exit path.
- PCRE2 automatic-callout tracing is a separate ABI and worker path. It copies
  only complete fixed records and bounded mark text, stops matching at the
  50,000-event or 10 MiB serialized trace cap, and is killed on timeout,
  cancellation or crash.
- PCRE2 runs only in mandatory UTF/UCP mode. Exact UTF-8 byte boundaries are
  normalized to editor UTF-16 offsets; lone surrogates are rejected instead of
  being silently replaced by browser encoding.
- Python starts only after the worker verifies Pyodide, CPython and `re`
  identities. Pattern, subject, flags and replacement remain string data passed
  to the fixed application bridge; no user-supplied Python source is evaluated.
  Native code-point boundaries are checked before conversion to editor UTF-16.
  Bounded replacement bytes use canonical base64 rather than content-dependent
  JSON escaping; declared length, request cap and canonical UTF-8 are validated
  before the output is exposed.
- Java starts only after the worker verifies the TeaVM bridge ABI, engine
  identity and compiled self-test. It exposes only fixed bounded
  `Pattern`/`Matcher` entry points; no JVM, Java class loading or user-supplied
  JavaScript is available. The runtime is TeaVM, not OpenJDK.
- PHP starts only after the worker verifies PHP 8.5.8, PCRE2 10.44 and the
  fixed bridge ABI. User values are JSON data written to a private virtual
  filesystem; the bridge chooses a safe delimiter, allowlists modifiers and
  invokes only fixed `preg_*` operations.
- The legacy Perl worker writes exact-key, byte-bounded JSON to a fixed MEMFS
  request path and invokes only the constant `RegexTools::run_request()` entry
  point. The bridge uses `no re 'eval'`, rejects `(?{...})` and `(??{...})`
  outside escaped literals/classes, pre-tokenizes its non-executable
  replacement grammar and streams bounded UTF-8 chunks from native match spans.
  Successful output uses a separate fixed binary file, never the JSON response;
  the worker verifies exact bytes, limit and canonical UTF-8 before decoding
  it. Replacement stops at the match/capture-row bound and leaves the
  unprocessed subject tail unchanged. WebPerl 0.09-beta's unmodified generated
  loader contains one `eval(code)` site in an optional
  JavaScript-interoperability import; the fixed bridge never loads or calls it.
  The checksummed asset passes the browser CSP smoke without broad
  `'unsafe-eval'`.
- Ruby evaluates only the fixed application bridge during worker startup.
  Pattern, subject, flags, limits and replacement travel in one bounded,
  length-prefixed file in the worker-local WASI memory filesystem. Native
  `String#gsub`/`String#sub` block iteration cannot materialize expanded user
  output: a fixed CRuby 4.0.0-compatible expander stops at the byte limit and
  writes that prefix to a separate bounded file. The inert JSON response
  contains metadata only and is assembled without per-code-point arrays. No
  user value is interpolated into source or sent through ruby.wasm's
  `RubyVM.wrap`/`toJS` JavaScript-eval conversion path. Unpaired browser
  surrogates are rejected before UTF-8 encoding. Ruby cold startup passes the
  strict-CSP release browser gate.
- C++ accepts only structured fixed Embind calls and is compiled with
  Emscripten `-sDYNAMIC_EXECUTION=0`; the generated module is checked for
  dynamic-source construction. Its libc++-compatible replacement expander
  appends directly through the UTF-8 byte cap and is parity-checked at load.
  Embind returns a structured JavaScript string rather than JSON, so there is
  no escape amplification; the 64 MiB native buffer and its bounded UTF-16
  conversion are the explicit peak-output copies.
- Go and Rust accept JSON data only through fixed exports. Each validates its
  request and enforces pattern, subject, result and output caps inside
  WebAssembly. Adapters measure exact encoded request bytes; the native request
  cap covers worst-case sixfold JSON escaping for every accepted string field,
  so control-heavy values retain the documented decoded field limits. Their
  native-compatible replacement token streams stop at the output cap without
  allocating a whole capture expansion. Bounded UTF-8 output crosses the JSON
  bridge as fixed 4:3 base64 rather than content-dependent escaped text. Rust
  additionally caps compiled-regex and DFA storage.
- .NET accepts bounded JSON only through fixed `[JSExport]` methods and never
  evaluates user input as managed or JavaScript source. A native regex timeout
  complements the supervisor timeout. Its fixed replacement parser streams
  native-compatible token fragments through the UTF-8 byte cap, verifies
  parity with `Match.Result` at load and transports output as canonical base64
  to avoid JSON escaping amplification. The shipped .NET host contains runtime
  debugging machinery, but normal production startup disables debugging and
  is verified under the documented CSP.
- Scala compatibility shares the TeaVM Java bridge. It introduces no Scala
  runtime, dynamic compiler or additional execution surface.
- Every bundled engine directory has a closed file set, deterministic
  metadata/checksums and licence material verified both before installation and
  in `dist/`. Runtime imports and fetches resolve only against the same-origin
  release path.
- Replacement templates are strings; executable callbacks are unavailable.
- Replacement/list token decorations, rows, chips, diagnostics and list
  evaluation work have separate presentation/operation caps; incomplete list
  results cannot be exported.
- Per-match replacement mapping executes no user code and is capped at 200
  matches, 2,000 materialized contributions and 50,000 token evaluations.
- Imported JSON is aggregate-size-bounded before parsing, then field-validated
  and never evaluated or auto-run. Export and IndexedDB save apply the same
  32 MiB aggregate UTF-8 document bound before materialization or persistence.
- Project and standalone-suite test subjects are not exported by default, and
  unit-test subjects require a separate opt-in before IndexedDB persistence.
- Unit-test add, update, clone and append-import paths share 1,000-case and
  32 MiB aggregate state caps.
- Unit-test assertion diagnostics never stringify complete large values and
  retain at most 2 KiB UTF-8 per case. Oversized replacement output is not
  copied into an exact current-result draft.
- Corpus file input is local, recognized UTF-8 text only. It is bounded before
  decoding, rejects NUL/binary input, runs sequentially in a dedicated
  cancellable supervisor and never modifies source files.
- Corpus JSON/CSV/NDJSON summaries never contain source, capture-sample or
  applied-output content. CSV cells are formula-injection protected. Local UI
  previews are explicitly bounded. Applied files require an explicit
  content-export acknowledgement; names are normalized against archive path
  traversal and collisions. Incomplete applied output cannot be downloaded or
  included in the all-document ZIP, whose bytes have a separate hard limit.
- Corpus documents, contents, outputs and results are never placed in project
  JSON or IndexedDB. Schema-v1 imports that attempt to inject corpus payloads
  are rejected.
- ECMAScript growth analysis preflights repetition count and UTF-8 bytes before
  allocating a generated string. The analysis worker is terminated on timeout,
  cancellation or crash; replacement timing also preflights its output bound.
  Analysis settings and results are neither persisted nor uploaded.
- Generated-case synthesis runs in its own killable worker under AST, depth,
  attempt, repetition and UTF-8 byte caps. Labels come only from bounded
  requests to the selected real engine worker; a wrong outcome, timeout, crash,
  cancellation or compile rejection is never converted into a verified case.
  Settings, discarded candidates and unselected results are ephemeral and
  never uploaded.
- Imported generated-test provenance is accepted only for the exact supported
  generator id/version and must agree with the test assertion outcome. Unknown
  provenance fields and future versions are rejected; editing a case removes
  its provenance.
- ECMAScript formatting consumes only exact accepted regexpp literal-token
  ranges and never treats PCRE2 as ECMAScript. Source/candidate syntax and
  execution use separate killable workers; compile failure, truncation,
  timeout asymmetry, engine-identity drift, incomplete tests or any semantic
  difference fails closed. Preview/results are ephemeral and apply requires an
  explicit confirmation.
- Subject minimization rejects lone surrogates, caps input at 1 MiB UTF-8,
  evaluation count at 2,000 and aggregate wall time at 60 seconds. Each
  predicate check runs in the selected killable engine worker. Timeout, crash,
  cancellation and worker error remain distinct; truncated results fail
  closed. Subjects and results are ephemeral and never uploaded or persisted.
- No backend, uploads, telemetry, remote corpus URL, CDN, remote font,
  arbitrary command line or unsafe HTML rendering exists. Application code
  does not evaluate user-controlled source in any language. The generic
  generated Pyodide loader contains an Emscripten dynamic-link branch that
  uses JavaScript `eval`, while the legacy WebPerl loader contains the optional
  interop site described above. Neither shipped fixed execution path exercises
  those branches, and both load under the CSP below without broad
  `'unsafe-eval'`.

Every enabled profile, including a cold Ruby start, passes the release CSP
equivalent to:

```text
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
connect-src 'self';
img-src 'self' data:;
font-src 'self';
style-src 'self' 'unsafe-inline';
object-src 'none';
frame-src 'none';
base-uri 'none';
form-action 'none';
```

The Toolbox shell currently requires inline styles. Corpus ZIP generation and
every engine pack use self-hosted modules/assets and local workers under the
existing `worker-src` and `connect-src` policy. Broad `'unsafe-eval'` must not
be added to accommodate an engine. The release gate blocks any future profile
that fails this strict-CSP browser matrix. `'wasm-unsafe-eval'` remains required
for the WebAssembly runtimes.

Report vulnerabilities privately to the repository owner before public issue
details are posted.
