Skip to content

Add RE2-backed safe-regex substrate (re2 native dependency)#563

Open
kriszyp wants to merge 2 commits into
mainfrom
kris/waf-re2-dep
Open

Add RE2-backed safe-regex substrate (re2 native dependency)#563
kriszyp wants to merge 2 commits into
mainfrom
kris/waf-re2-dep

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Introduces compileSafeRegex (security/safeRegex.ts), a linear-time regex compiler backed by the re2 native addon, as the safe substrate for any regex whose source is operator- or attacker-influenced. RE2 is a finite-automaton engine with a linear-time matching guarantee, so a pathological pattern such as (a+)+$ cannot trigger catastrophic backtracking and stall the event loop the way new RegExp(...) can (ReDoS).

This is deliberately split out from #517 (the WAF component) so the native-dependency introduction is reviewed on its own terms — prebuilt-matrix resolution, install-time behavior, and the linear-time/feature-rejection contract — separate from the WAF matcher logic. The WAF PR will then swap its compileRuleRegex internals onto this substrate and delete its interim JS-RegExp alternation-gate machinery.

What's in it

  • security/safeRegex.tscompileSafeRegex(source, where, errors): RegExp | undefined. Signature mirrors an error-collecting validator. The returned object is an RE2, which extends RegExp, so it is drop-in for the .test() / .exec() surface at call sites.

  • unitTests/security/safeRegex.test.mjs — 5 tests pinning the two properties that justify the dependency:

    1. ReDoS immunity(a+)+$ against a 50k-char non-matching input completes in <250ms (measured ~0.08ms; RegExp would not return within the test timeout).
    2. Feature rejection is part of the contract — backreferences (\1) and lookaround ((?=…)) are unsupported in linear-time RE2 and surface as compile errors, alongside ordinary invalid-syntax errors. Callers treat "unsupported feature" and "invalid syntax" the same way: the rule does not compile.

    Loading the module at all also exercises that the re2 native binary resolves on the CI matrix.

  • package.json / package-lock.json"re2": "^1.26.0" plus its install-time subtree (node-gyp/tar/install-artifact-from-github) used only for the source-build fallback.

Prebuilt-matrix coverage

re2 publishes prebuilt binaries covering every ABI Harper targets (engines: ^22.18.0 || >=24.0.0):

ABI Node linux x64 linux arm64 linux-musl x64 linux-musl arm64 darwin x64/arm64 win32 x64/arm64
127 22
137 24
147 25

Harper's Docker bases (Debian node:, RHEL UBI, Ubuntu-CUDA) are all glibc → covered; musl is covered too if an Alpine variant is ever used.

Residual risks (not blockers)

  1. nan + fetch-from-GitHub model. re2 binaries are Node-major-specific and fetched from GitHub releases at install (not the npm registry). A future Node major landing before re2 publishes its matching ABI would force a node-gyp source build — the ongoing tax of any nan-based addon.
  2. Build egress. A GitHub-egress-blocked build falls back to a source compile (needs a C++ toolchain, present in the Docker build stage). Harper's build-tools/download-prebuilds.js is lmdb/msgpackr-specific; re2 self-fetches during npm ci, so no wiring is needed there.

Note for the WAF follow-up

re2 also ships RE2.Set — a native multi-pattern set matcher (new RE2.Set([...]), returns matched pattern indices). That is the correct replacement for #517's combined-alternation pre-gate: no capture-group renumbering, so the hasBackreference / gateable-vs-ungated split machinery deletes entirely.

Tests & checks

  • 5/5 unit tests pass against the .ts source with the real re2 binary (Node 24 / ABI 137, linux x64).
  • oxlint clean, prettier clean.

Generated with Claude Code (Opus 4.8), reviewed by Kris.

Introduce compileSafeRegex (security/safeRegex.ts), a linear-time regex
compiler backed by the re2 native addon, as the safe substrate for regex
whose source is operator- or attacker-influenced. RE2's finite-automaton
engine cannot catastrophically backtrack, so a pattern like (a+)+$ cannot
stall the event loop (ReDoS) the way new RegExp(...) can.

The signature mirrors an error-collecting validator; RE2-unsupported
features (backreferences, lookaround) surface as compile errors rather than
running unsafely -- that rejection is part of the contract.

This lands the native dependency on its own so its prebuilt-matrix
resolution and the linear-time/feature-rejection contract are reviewed
independently of the WAF logic that will consume it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp requested a review from a team as a code owner July 11, 2026 13:47
@socket-security

socket-security Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedre2@​1.26.08810010094100

View full report

@socket-security

socket-security Bot commented Jul 11, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm re2 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/re2@1.26.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/re2@1.26.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new utility function compileSafeRegex in security/safeRegex.ts that uses the re2 library to compile regular expressions safely, protecting the application against catastrophic backtracking (ReDoS) attacks. It also adds comprehensive unit tests to verify linear-time matching and the rejection of unsupported features like backreferences and lookarounds. The feedback points out a potential issue in the error handling of compileSafeRegex where a non-Error object could be thrown, which would cause a secondary TypeError when accessing .message. It is recommended to check if the caught error is an instance of Error before accessing its properties.

Comment thread security/safeRegex.ts
Comment on lines +25 to +27
} catch (error) {
errors.push(`${where}: invalid or unsupported regex ${JSON.stringify(source)}: ${(error as Error).message}`);
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In JavaScript/TypeScript, any value can be thrown (not just instances of Error). If a non-Error value (or null/undefined) is thrown, attempting to access (error as Error).message will result in a secondary TypeError (e.g., Cannot read properties of null (reading 'message')), which would crash the function and bypass the safety of the try-catch block. It is safer to check if the caught error is an instance of Error before accessing its message property, falling back to a string representation otherwise.

	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		errors.push(`${where}: invalid or unsupported regex ${JSON.stringify(source)}: ${message}`);
		return undefined;

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

kriszyp added a commit that referenced this pull request Jul 11, 2026
…ion gate

Point compileRuleRegex at the RE2-backed compileSafeRegex (harper-pro #563)
so operator-supplied patterns match in guaranteed linear time — closing the
ReDoS vector the prototype's `new RegExp(...)` left open (a hostile `(a+)+$`
can no longer stall the event loop).

Because every path-regex is now a non-backtracking RE2, the JS-RegExp
combined-alternation pre-gate and its backreference-safety machinery
(hasBackreference + gateable/ungated split) exist only to make an unsafe
engine fast, so they are deleted: path-regex rules are matched by a plain
linear scan, still allocation-free on the no-match path. For realistic
super_user-authored rule sets this scan is negligible; RE2.Set is the
drop-in native multi-pattern scan if path-regex volume ever dominates.

Contract change: RE2 cannot evaluate backreferences or lookaround, so a rule
using them is now rejected as invalid (same as malformed syntax) rather than
silently compiled. The M3 adversarial tests are rewritten to pin this
rejection and the ReDoS immunity in place of the removed gate's behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unit/integration/stress workflows install with `npm install --ignore-scripts`
and then run Harper. lmdb/msgpackr/rocksdb survive that because they bundle their
prebuilt binary in the npm package; re2 instead downloads its binary from a
post-install script, so --ignore-scripts leaves re2.node absent and every Harper
process that loads the WAF fails with "Cannot find module './build/Release/re2.node'".

Add an explicit `npm rebuild re2` step after each such install to materialize the
binary (fetches the prebuilt where one exists, source-builds otherwise — e.g. on
Node majors re2 has not yet published a prebuilt for).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp requested a review from a team as a code owner July 11, 2026 16:40
kriszyp added a commit that referenced this pull request Jul 11, 2026
…ion gate

Point compileRuleRegex at the RE2-backed compileSafeRegex (harper-pro #563)
so operator-supplied patterns match in guaranteed linear time — closing the
ReDoS vector the prototype's `new RegExp(...)` left open (a hostile `(a+)+$`
can no longer stall the event loop).

Because every path-regex is now a non-backtracking RE2, the JS-RegExp
combined-alternation pre-gate and its backreference-safety machinery
(hasBackreference + gateable/ungated split) exist only to make an unsafe
engine fast, so they are deleted: path-regex rules are matched by a plain
linear scan, still allocation-free on the no-match path. For realistic
super_user-authored rule sets this scan is negligible; RE2.Set is the
drop-in native multi-pattern scan if path-regex volume ever dominates.

Contract change: RE2 cannot evaluate backreferences or lookaround, so a rule
using them is now rejected as invalid (same as malformed syntax) rather than
silently compiled. The M3 adversarial tests are rewritten to pin this
rejection and the ReDoS immunity in place of the removed gate's behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@cb1kenobi did you see @uhop built this re2 package? Good job Eugene! More Dojo contributors that have gone on to build native node modules :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant