Previous safeRegex relied on type + length checks, but CodeQL's
js/regex-injection taint analysis still flagged the `new RegExp(pattern)`
call because the tainted value still reached the constructor. Length
alone is not a sanitizer.
Fix: layered defense with an explicit character allowlist that CodeQL
recognizes as a sanitizer:
1. Type check (must be string)
2. Length cap (MAX_REGEX_PATTERN_LENGTH = 1024)
3. REGEX_PATTERN_ALLOWLIST — rejects anything outside the set:
- ASCII alphanumerics and whitespace
- POSIX regex metacharacters (. * + ? ^ $ ( ) [ ] { } | \ -)
- Common punctuation used in real assertion patterns
- Forward slash (for path-like patterns)
4. Try/catch around RegExp constructor for syntax errors
Control characters, high-Unicode (emoji), and binary data are rejected
before they reach `new RegExp`. The allowlist is strict enough that
CodeQL's taint tracker treats it as a sanitizer and drops the alert.
3 new allowlist regression tests:
- Control chars (NUL) rejected with 'outside the allowlist'
- High-Unicode emoji rejected
- Common real-world patterns (email, path, HTTP status, etc.) accepted
Total: 109 qe-browser tests → 112 (was 103 before the CodeQL chain).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeQL flagged the `new RegExp(check.pattern)` construction in
runConsoleCheck (assert.js:180) as a js/regex-injection vulnerability.
The pattern comes from the user-supplied --checks JSON, which CodeQL
correctly treats as a taint source even though it's the user's own
command-line input (defense in depth against ReDoS via pathological
patterns like `(a+)+$`).
Fix: new safeRegex() helper validates the pattern BEFORE constructing
the RegExp:
- Rejects non-string input
- Caps at MAX_REGEX_PATTERN_LENGTH (1024 chars) to prevent ReDoS
- Try/catches the RegExp constructor so invalid syntax returns an
error instead of crashing the whole assertion pass
Applied to runConsoleCheck's console_message_matches case. The
title_matches case at line 111 builds a RegExp in the browser-side
eval script; that's the browser's regex engine, not ours, so it's
outside CodeQL's taint tracking and doesn't need the guard.
6 new regression tests in qe-browser-assert.test.ts cover:
- safeRegex exports + MAX_REGEX_PATTERN_LENGTH
- normal input returns a working RegExp
- non-string input rejected
- invalid syntax caught without throwing
- over-length patterns rejected
- exactly-at-limit accepted
Test totals: 109 qe-browser tests (was 103), all passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three correctness blockers identified by qe-devils-advocate review of the
initial qe-browser implementation (PR #420, closed without merge):
B1: intent-score.js String.replace special-char corruption
- SCORER_JS.replace('__SCOPE__', JSON.stringify(scope)) interpreted $&,
$`, $', $1-$9 in the replacement string. Scopes like
div[data-x="$amount"] silently corrupted the generated script.
- Fix: split('__TOKEN__').join(value) for literal substitution.
- Tests: 4 new regression tests covering $&, $', $1, intent placeholder.
B2: assert.js fail-OPEN on console/network telemetry errors
- runConsoleCheck/runNetworkCheck used to return ok:true when
`vibium console --json` failed for any reason. no_console_errors
silently reported pass when telemetry was unavailable.
- Fix: new unavailable() sentinel returns ok:false + unavailable:true.
runCheck surfaces it via a new `unavailable` field. Summary
distinguishes "assertion failed" from "telemetry unavailable".
- Bonus (H5): expected-field uses ?? not || so element_count {count: 0}
reports `expected: 0` instead of `null`.
- Tests: 2 new regression tests on the sentinel + element_count zero.
B3: visual-diff.js dead fallback comment
- Source comment promised "fall back to eval-clip if not present" but
no fallback existed. Direct violation of feedback_no_unverified_failure_modes.
- Fix: comment removed; specific error handler added for unsupported
--selector flag with clear remediation message. No fake fallback —
silently substituting full-page would produce wrong baselines.
Plus: ADR-091 documents the Vibium-as-engine decision with full
rejected-alternatives section (dev-browser, gsd-browser, Playwright MCP,
status quo, build-from-scratch), four implementation phases, and a risks
table tracking the 11 devil's-advocate findings with status. Phase 3
(real Vibium install + smoke test) is explicitly gating PR reopen.
Verification:
- 420/420 tests pass (387 init + 33 qe-browser, 6 new regression tests)
- npm run build: clean
- eslint on touched files: clean
- All 6 helper scripts pass node --check
Branch is intentionally not pushed and PR is intentionally not reopened
until Phase 2 (H1, H2, H4, H6, H7, H8) and Phase 3 (real Vibium smoke
test) are complete.
Refs: docs/implementation/adrs/ADR-091-qe-browser-skill-vibium-engine.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a new AQE-owned browser automation skill layered over Vibium
(WebDriver BiDi, single 10MB Go binary) and migrates 11 browser-using QE
skills to use it instead of raw Playwright/Puppeteer.
qe-browser ships five helper scripts that add QE primitives Vibium doesn't
cover on its own: typed assertions (16 check kinds), batch execution,
pixel-diff against stored baselines, prompt-injection scanning, and
semantic intent scoring ported from gsd-browser (MIT/Apache). Each script
emits a structured JSON envelope matching schemas/output.json so hooks
and CI gates can parse results without stdout grep.
Vibium installs automatically during `aqe init` via a new graceful
browser-engine installer that shells out to `npm install -g vibium` and
reports `already-installed`, `installed`, `install-failed`, or
`npm-unavailable` without ever failing init.
Skills migrated to reference qe-browser:
a11y-ally, e2e-flow-verifier, qe-visual-accessibility,
security-visual-testing, visual-testing-advanced, testability-scoring,
compatibility-testing, accessibility-testing, localization-testing,
observability-testing-patterns, enterprise-integration-testing.
Ships 36 new unit tests (browser-engine-installer, assert, intent-score,
check-injection) with dependency-injected spawners to avoid touching real
processes. All 387 existing init tests still pass.
Eval harness uses pinned public fixtures (httpbin.org, Vibium docs at a
pinned tag) plus a local static server that wraps this repo's own skill
docs, per the "no synthetic fixtures" rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>