feat(cli): port CRS 942100/941100 on libinjection compiled to WebAssembly - #239
Merged
Merged
Conversation
ThreatCrush Security Scan12 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5
Snippets are redacted; ThreatCrush never prints matched credential material. |
…Assembly CRS's @detectSQLi (942100) and @detectXSS (941100) were skipped because they need libinjection. Vendor libinjection v4.0.0 (BSD-3-Clause, the commit ModSecurity v3 builds against) unmodified, compile it with zig 0.16.0 to a self-contained wasm32-wasi reactor module (no imports) through scripts/build-libinjection-wasm.mjs, and check the module in so building needs no C toolchain. `--check` rebuilds and compares the hash; a new CI job runs it. The generator now ports both rules with CRS's own targets and transformations, so a bare `1' OR 1=1` or `admin'--` is banned. The module ships in dist/libinjection/ with its licence and resolves from the bundle. Public rule counts move from 94 to 96 via the doc-claims mechanism.
ralyodio
force-pushed
the
feat/crs-libinjection-wasm
branch
from
September 25, 2026 18:25
29bce1f to
8d0cc93
Compare
ralyodio
added a commit
that referenced
this pull request
Sep 25, 2026
…refresh release docs (#238) * ci(desktop): sign and notarize when secrets exist; declare libgbm1/ALSA in the deb - scripts/desktop-signing-env.sh exports electron-builder's signing variables only for secrets that are set (APPLE_CERTIFICATE[_PASSWORD], APPLE_API_KEY/ _KEY_ID/_ISSUER or APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID, WINDOWS_CERTIFICATE[_PASSWORD]); without them builds stay unsigned - drop notarize: false so notarization follows the credentials - package (never publish) the desktop matrix on PRs touching desktop packaging - deb.depends adds libgbm1 and libasound2t64 | libasound2: the published deb failed on minimal installs with libgbm.so.1 missing - docs: desktop packaging has been green since v0.13.2; refresh release, surface and mobile status from gh evidence - web: Download Desktop links pointed at a 404 /releases; state that macOS/Windows builds are unsigned * ci(desktop): leave pull-request packaging runs unsigned electron-builder already skips macOS signing on PRs (CSC_FOR_PULL_REQUEST); treat Windows the same so PR runs never decode the certificates, and say so in the job summary instead of claiming signing is on. * docs: record extension and libinjection status; soften unsigned-build wording SURFACES.md picks up the browser-extension (#240) and libinjection/WASM (#239) status lines and the extension-store blockers; RELEASE_STATUS.md lists the store accounts. The homepage now says unsigned macOS/Windows builds may be blocked, not only warned about, on first launch. * docs: mark the extension page checks (#240) as merged in surface and release status
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
This ports CRS 942100 (
@detectSQLi) and 941100 (@detectXSS). The CRS port (#222) skipped both because they need libinjection. They now run on libinjection itself, compiled to WebAssembly and shipped in the npm package. A bare1' OR 1=1oradmin'--in a query argument used to score 0. Now it's banned.apps/cli/vendor/libinjectionholds libinjection v4.0.0 (commit2117822, BSD-3-Clause), unmodified:src/library files,COPYING, and a subset of upstream test vectors. This is exactly the commit ModSecurity v3 master pins as itsothers/libinjectionsubmodule.scripts/build-libinjection-wasm.mjs(pnpm --filter @profullstack/threatcrush libinjection:build) compiles the vendored sources and a small interface file,scripts/libinjection-wasm.c, with zig 0.16.0. It targetswasm32-wasiin the reactor model with-O ReleaseFast -fstrip -DNDEBUG, and zig's wasi-libc is linked statically. The result,src/core/crs/libinjection/libinjection.wasm(180,356 bytes, sha25680ec3107…2f33), imports nothing; the script refuses to write a module that does.NDEBUGcompiles out libinjection'sassert()s, which would otherwise pull in WASIfd_write/proc_exit. The script requires exactly zig 0.16.0 (ZIG_VERSION).--check: rebuilds and compares the hash, the same idea as the CRS generator's--check. A new CI job,libinjection.wasm is reproducible, installs zig 0.16.0 (mlugg/setup-zig@v2) and runs it. Building and testing the CLI still needs no C toolchain. I rebuilt from a copy of the tree in another directory with an empty zig global cache and got the same hash.src/core/crs/libinjection.tsexposesdetectSQLi(bytes) → { result, fingerprint }anddetectXSS(bytes) → result. Both take byte strings, one char per byte, as the engine uses everywhere. The module is compiled and instantiated synchronously (new WebAssembly.Module(bytes)) when aCrsEngineis built. It lives atlibinjection/libinjection.wasmnext tolibinjection.tsinsrc/, and next to the bundle indist/(tsup copies it together withCOPYING), so a single__dirname-relative path works in both places.filesinpackage.jsonnow includesdist/libinjection/libinjection.wasmanddist/libinjection/COPYING.@detectSQLi/@detectXSSare now supported operators, so both rules go through the generator with CRS's own targets and transformations:REQUEST_HEADERS:User-Agent|REQUEST_HEADERS:Referer|ARGS_NAMES|ARGS,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatchREQUEST_HEADERS:User-Agent|ARGS_NAMES|ARGS,t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNullsisMaliciousLibinjectionResult), libinjection'sERRORresult counts as a match (fail-safe). It never came up in any corpus below.doc-claims.test.tsderives the number.auto-defence.mdnow describes both rules and libinjection's known false-positive shapes.NOTICEmentions libinjection and where its licence ships.No new npm dependencies.
libinjection's own test vectors
tests/test-sqli-*.txt(testdriver type 2: fingerprint)libinjection.wasmvia the TS wrappertests/test-{tokens,folding,html5}-*.txttestdriver.c+ the same sources, same zig 0.16.0 flags (wasm32-wasi -O ReleaseFast -DNDEBUG), undernode:wasi; these tests need tokenizer internals the shipped module doesn't exportdata/sqli-*.txt(reader -i -m 18)data/false_positives.txt(reader -m 21)data/xss*(reader -t -i -x -m 20)reader.cThe total is 584/584 of
tests/, with all three sample suites passing at upstream's thresholds. Committed assrc/core/crs/__tests__/libinjection.test.ts(BSD-3-Clause permits it, withCOPYINGvendored alongside): all 50test-sqlivectors, plusfalse_positives.txtand 7 smaller sample files, checked against upstream's thresholds. There's also a test for bytes above 0x7f and for 100 KB inputs, which grow the module's memory.Real-log replay, before vs after
The corpus is much smaller than earlier PRs'. The user's sudo setup script ran
logrotate -f /etc/logrotate.d/nginx27 times between 17:15 and 17:35 today, soaccess.log.1…14.gznow cover about 20 minutes and the two weeks of history earlier PRs replayed (212k requests) is gone; no copy exists. I replayed everything left in/var/log/nginx: the currentaccess.logand its rotations, plus every vhost's access logs (alt.2600.*back to Sep 11,seo.rank.*,moshpit-parking.*,chovy.hacker.*,auto.hacker.*). The replay goes through the reallog-parser(parseNginxLog→assessNginxRequest→attackSeverity),origin/mastervs this branch.I hand-classified all 40 (40 distinct request shapes): one scanner,
45.148.10.95, probing 40 credential paths (/.aws/credentials,/aws.yml,/credentials.json,/terraform/aws.tf…) with?id=%00&exemple=<svg/onload=confirm(1)>. All 40 are attacks, and every one was already at ban level through 941120/941160/941390. 0 legitimate requests are affected.Synthetic benign corpus (to make up for the thin log set)
These target libinjection's known false-positive shapes. The replay uses both bundles and puts each value in the Referer too.
/search?q=percent-encoded and+-encoded, a JSON-ishfilter=,name=). The values cover apostrophes in names and search terms (O'Brien,D'Angelo,L'Oréal,McDonald's,Rock 'n' Roll,don't stop believin',Guns N' Roses,'90s,5' 10"…), quotes ("exact phrase",she said "yes"), SQL words in prose (where is george,union station,select all,1 or 2,true or false,drop table saw…), JSON in parameters ({"status":"open","n":1},{"q":"O'Brien"},[1,2,3]…), and odds and ends (C++ vs C#,AT&T,#hashtag, emails, dates, non-ASCII).state/code, JWTs,redirect=, phone numbers, addresses,filter[status]=open…), 24 common User-Agents (browsers, Googlebot, bingbot, curl, python-requests, facebookexternalhit, Slackbot…) and 9 Referers (Google, DuckDuckGo, t.co, HN, Reddit, android-app…).Result: everything with apostrophes, quotes, SQL prose, JSON, User-Agents and Referers scores exactly as before. 8 synthetic requests (2 values) newly reach ban level:
50% off -- today only1c--commentitem /* note */nc/* */commentProbing further: a number directly followed by
--(2019 -- 2020,10--20,3 -- 4) gets1c, whilepage 10--20,a -- b,Spider-Man -- Homecoming,sale -- 50% offand/* hi */don't match. This is what libinjection does under ModSecurity + CRS too. None of these shapes appeared in the real logs, so I haven't excluded anything.auto-defence.mdnames the shapes and the CRS-sanctioned remedy (exclude_rules = [942100], i.e.SecRuleRemoveById) for a site whose visitors search for--number ranges.Detection gain
These are libinjection's own sample inputs sent through the whole engine as a query argument:
Throughput (real logs, 24,370 lines)
Both bundles run in one process, in alternating passes, each pass with a fresh engine and an empty memo, 25 passes each, measured in CPU time. The machine was loaded (load average ~13), so wall-clock numbers were noisy.
That's about 10% slower. Engine construction, including compiling the wasm, took ~50 ms both before and after (fresh-process median of 21 runs; no difference within noise).
Verification
pnpm --filter @profullstack/threatcrush test: 31 files, 360 tests pass.tsc --noEmitis clean.build-crs-rules.mjs --checkandbuild-libinjection-wasm.mjs --checkpass.Failing before, passing after: with
origin/master's rule table,log-parser.test.tsfails "bans tautology and comment SQLi that only libinjection sees (CRS 942100)" and "scores XSS libinjection finds (CRS 941100) …". Both pass on this branch. Those tests cover1' OR 1=1(percent-encoded and nginx's\x27),admin'--,1 or 2 1.e/1, SQLi in the User-Agent and the Referer, and"><svg onload=alert(1)>in an argument, an argument name and the User-Agent. A third test keepsO'Brien, quotes, SQL prose and JSON (with the same URL as Referer) at score 0.From
dist/: I ranpnpm --filter @profullstack/threatcrush buildand thennode apps/cli/dist/index.js monitor -m log-watcheragainst the live/var/log/nginx/access.log, and sent three requests to the local nginx:GET /login?user=1%27%20OR%201=1:[HIGH] Attack detected [SQLI]GET /login?user=admin%27--:[HIGH] Attack detected [SQLI]GET /search?q=O%27Brien:INFO … → 301That shows the wasm resolving from
dist/libinjection/.npm pack --dry-runlistsdist/libinjection/libinjection.wasm(180.4 kB) anddist/libinjection/COPYING.Residual risk
--, or a word followed by/* … */, in an argument, argument name, User-Agent or Referer, now bans. None occurred in the real traffic available, but that traffic is thin (see above). Operators can setexclude_rules = [942100].NDEBUGremoves libinjection's internalassert()s. The ones in v4.0.0 guard invariants (my_memmemarguments,pos >= 3), not input validation, and every upstream vector passes.mlugg/setup-zigbeing able to download zig 0.16.0.