Skip to content

fix(security): adjudicate open CodeQL alerts (#1281) - #1291

Merged
SisyphusZheng merged 2 commits into
devfrom
fix/1281-codeql-alert-adjudication
Sep 3, 2026
Merged

fix(security): adjudicate open CodeQL alerts (#1281)#1291
SisyphusZheng merged 2 commits into
devfrom
fix/1281-codeql-alert-adjudication

Conversation

@SisyphusZheng

Copy link
Copy Markdown
Member

Problem

Issue #1281: 11 CodeQL security alerts have been open on main since Aug 2026 (they ship in stable 0.43.3; they surfaced as "new" during the Beta.1 dev→main promotion only because the promotion diff is the entire v0.44 train). Beta.2 hardening requires each alert to be either fixed with test evidence or dismissed with a technically defensible, externally visible rationale.

Owner

Single-owner security-hygiene slice: static sanitizers, static-file serving, and docs-gate tooling. No second owner was involved because no runtime architecture, public API, or frozen-semantics path is touched (deno task freeze:semantics:check green — no ADR-0122 paths changed, so no ADR-0151 authorization was required).

Per-alert adjudication

Alert Rule File Verdict Reason
#4143 incomplete-multi-character-sanitization packages/element/src/internal/core/html-escape.ts Fix Script-block strip now runs to a fixed point before the strip-to-EOF backstop. A single pass could emit a re-formed <script>…</script> and the backstop then ate all trailing markup.
#4144 bad-tag-filter same Fix End-tag pattern is now <\/script(?=[\s/>])[^>]*> — browsers ignore attributes on end tags (</script\t\n bar> still ends the raw-text element).
#4140 incomplete-multi-character-sanitization same Fix on* attribute strip runs to a fixed point; a single pass could concatenate a leftover o prefix with an nclick=… suffix into a live onclick handler.
#4142 bad-tag-filter same Fix Comment-balance check counts --!> (HTML-standard abrupt close) in addition to -->. Diagnostic heuristic only, not a security boundary.
#4151 incomplete-multi-character-sanitization www/app/site-ui/article-body.ts Fix prepareArticle heading labels now drop stray </> the tag pattern cannot match, so a partial-tag fragment (e.g. <script with no >) can never reach the rail outline.
#4150 file-system-race packages/adapter-vite/src/internal/static-serve.ts Fix tryStatic reads each candidate directly with failure-as-miss fallback (try/catch readFileSync) instead of existsSync/statSync guard-then-read TOCTOU. Mirrored in the generated serve.mjs template (ssg-helpers.ts) so the two servers cannot drift.
#4148 insecure-temporary-file packages/adapter-vite/src/internal/content/sitemap/generator.ts Dismissed (false positive) Write target is the developer's own SSG output dir, not a shared temp dir; mode: 0o644 pinned; truncate-overwrite is the required regeneration contract (O_EXCL would break rebuilds); the only os.tmpdir() dataflow is from test fixtures.
#4149 insecure-temporary-file same (robots.txt write) Dismissed (false positive) Same rationale as #4148.
#4131 incomplete-sanitization tools/check-public-docs-integrity.ts Fix Prerelease tag is now embedded via the repo-standard escapeRegExp (tools/lib/text.ts) instead of a dot-only replace. Provably behavior-identical for the constrained tag shape [a-zA-Z]+\.\d+ (parity pinned by test).
#4145 template-syntax-in-string-literal tools/autoflow/__tests__/release.test.ts Dismissed (used in tests) The flagged string is intentional fixture data mirroring the real project-constants.ts line PACKAGE_VERSION_TAG = `v${PACKAGE_VERSION}` as input to bumpProjectConstantsText; it must not interpolate. Rewriting it would reduce fixture fidelity.
#4170 useless-assignment-to-local packages/app/__tests__/spa-projection-guard.test.ts Fix Dropped the always-overwritten baseline = Object.prototype initializer.

The 8 fixed alerts auto-close when the next CodeQL scan of dev no longer finds them; the 3 dismissals are already recorded on the alerts with these rationales.

Before / After

  • Before: single-pass regex strips in sanitizeHeadExtras that (a) missed spec-valid script end tags with attributes, (b) could emit sanitizer-created live markup in adversarial nesting cases, and (c) a destructive strip-to-EOF fallback that ate legitimate trailing markup; check-then-read static file serving; a docs-gate RegExp interpolation with partial escaping; a useless assignment in a test.
  • After: fixed-point, spec-accurate strips with the EOF backstop kept as final safety only; read-and-fallback static serving in both the CLI server and the generated standalone server; repo-standard escapeRegExp; clean test code. The escape/sanitize security contract is unchanged in the safe direction only: everything the old code stripped is still stripped; the new code additionally strips residuals the old code missed.

Why-not-second-owner

No frozen-semantics path (ADR-0122) is touched; no public API, protocol, or compiler contract changes. html-escape.ts is not on the frozen list, and its changes are contract-preserving strengthenings proven against the existing security-projection suites plus new adversarial regression tests. A second owner would add review latency without a second architecture domain.

Evidence

  • RED first: 4 new adversarial html-escape tests + 2 new article-body tests fail on the old code, pass on the fix (packages/element/__tests__/html-escape.test.ts 10/10, www/__tests__/article-body.test.ts 3/3).
  • Security-projection parity: packages/element/__tests__ 253/253 (incl. compiled-escape-parity, sanitize); deno task check:static-output-freeze -- --self-check PASS (655 files byte-identical across runs). The full baseline-comparison variant errors on a pre-existing environment issue (the v0.41.2 baseline worktree cannot resolve npm:sanitize-html); that failure is in the baseline build path and unrelated to this change.
  • Static serving: static-serve, ssg-helpers, serve-entry (boots the regenerated serve.mjs over HTTP — dynamic + static channels green), sitemap-generator, head-injection, entry-renderer, ssg-admission-parity, start-request-containment: 123+ green; directory-candidate parity test added.
  • Tools: tools/ 479/479; deno task docs:check-public passed.
  • App: packages/app/__tests__ 99/99.
  • Publishability: deno task pack:dry-run exit 0; deno task package-artifacts:check exit 0.
  • Full deno task test: green (see checks).

Scope

In: the 11 open alerts enumerated live via gh api repos/open-element/openelement/code-scanning/alerts?state=open (matches the #1281 list exactly). Out: any other CodeQL rule tuning, sanitizer contract changes, frozen-semantics paths, release/tag work.

Risk

Low. All behavior changes are in the safe direction (strip more residual adversarial markup; fail closed on vanished files). Normal-content parity is pinned by the pre-existing suites plus new tests (ordinary headings, meta preservation, directory-miss parity, fixture server and standalone server contract tests).

Closes #1281

Fixes (8 alerts):
- element html-escape.ts (#4140/#4142/#4143/#4144): headExtras script strip
  now runs the block pass to a fixed point before the strip-to-EOF backstop
  (a single pass could emit a re-formed <script> or a live on* handler that
  the strip itself concatenated); the script end-tag pattern accepts
  attributes/whitespace per the HTML spec; the comment-balance check counts
  --!> as a close. Contract preserved: scripts/handlers are still stripped
  when allowHeadExtrasScripts is false; adversarial regression tests added.
- adapter-vite static-serve.ts (#4150): tryStatic reads candidates directly
  with failure-as-miss fallback instead of existsSync/statSync
  guard-then-read (TOCTOU). Mirrored in the generated serve.mjs template
  (ssg-helpers.ts); directory-candidate parity pinned by test.
- www site-ui article-body.ts (#4151): prepareArticle heading labels drop
  stray angle brackets the tag pattern cannot match, so a partial tag
  fragment can never reach the rail outline.
- tools check-public-docs-integrity.ts (#4131): stale-claim guard embeds the
  previous prerelease tag via the shared escapeRegExp (repo-standard full
  metacharacter escape); parity pinned by test.
- app spa-projection-guard.test.ts (#4170): drop the always-overwritten
  baseline initializer.

Dismissed via gh api with recorded rationale (see issue #1281):
- #4145 (used in tests): release.test.ts fixture intentionally embeds a
  template literal as text data mirroring project-constants.ts.
- #4148/#4149 (false positive): sitemap/robots writes target the developer's
  own dist output dir with pinned mode 0o644; overwrite-on-rebuild is the
  intended contract; tmpdir dataflow exists only in test fixtures.

No ADR-0122 frozen-semantics paths touched (freeze:semantics:check green).
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

Comment thread www/app/site-ui/article-body.ts Fixed
…issue ref from comment (#1281)

CodeQL js/incomplete-multi-character-sanitization re-fired on the
single-pass tag strip in prepareArticle (new alert on PR #1291); apply the
documented fixpoint idiom, matching the html-escape.ts sanitizeHeadExtras
structure. The www theme-token gate also read '(#1281)' on a line containing
'outline' as a hex color literal; reworded.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying openelement with  Cloudflare Pages  Cloudflare Pages

Latest commit: 03c801f
Status: ✅  Deploy successful!
Preview URL: https://86358420.lessjs.pages.dev
Branch Preview URL: https://fix-1281-codeql-alert-adjudi.lessjs.pages.dev

View logs

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

@SisyphusZheng
SisyphusZheng merged commit 493cf2a into dev Sep 3, 2026
10 checks passed
@SisyphusZheng
SisyphusZheng deleted the fix/1281-codeql-alert-adjudication branch September 3, 2026 14:58
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.

2 participants