Files
regex-tools/docs/SECURITY.md

179 lines
11 KiB
Markdown

# 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.