Skip to content

fix(security): clear CodeQL self-gate (complete sanitization, no double-escaping, atomic FS ops)#39

Merged
alex-mextner merged 1 commit into
mainfrom
codeql-selfgate-fix
Jun 17, 2026
Merged

fix(security): clear CodeQL self-gate (complete sanitization, no double-escaping, atomic FS ops)#39
alex-mextner merged 1 commit into
mainfrom
codeql-selfgate-fix

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

Why

The CodeQL self-gate (security-extended, tier: block in rig.yaml) was RED on main, blocking every tg-cli PR — including the socket-leak crash-fix #37. Root cause: 18 gated warning-level findings (8 in product code, 10 js/file-access-to-http in by-hand dev scripts).

What

All findings fixed properly — no in-source suppression of the product-code bugs.

Rule Site Fix
js/incomplete-multi-character-sanitization transmitter.ts, rich.ts /<[^>]+>/g left a dangling <script; new shared stripHtmlTags() also strips a trailing tag-like fragment, one pass, preserving a lone literal <
js/double-escaping transmitter.ts, table.test.ts chained entity .replace()s could chain-decode (&amp;lt;); new shared decodeHtmlEntities() consumes each entity once via a lookup table
js/incomplete-sanitization rich.ts RICH_TAG_RE regex built escaping only -; new shared escapeRegExp() escapes the full ES metachar set (install-skill's local escapeRegex migrated onto it)
js/file-system-race install.ts, telegram.ts TOCTOU check-then-read/-write replaced with atomic ops: readTextIfExists() (single read, catch ENOENT) and readSmallFile() (one fd: open→fstat→read→re-fstat guard); .bak written from the in-memory snapshot
js/file-access-to-http (×10) scripts/*.ts gate scoped to the shipped product surface via .github/codeql/codeql-config.yml paths-ignore: scripts — by-hand emoji/sticker generators, never shipped, no untrusted input. features/ keeps full coverage incl. this query

New shared helpers: features/render/html.ts (stripHtmlTags, decodeHtmlEntities), features/util/regex.ts (escapeRegExp).

Tests

bun test: 1031 pass / 0 fail. New: tests/html-sanitize.test.ts, tests/read-small-file.test.ts, plus visibleLength integration cases pinning the dangling-tag and &amp;lt; edge cases the findings flagged.

Review

Pre-commit review --staged -m kimi,glm,qwen,minimax,deepseek (3 rounds). The opencode-backed models (kimi/qwen/minimax/deepseek) were down (provider 500s / db-locked) throughout; GLM reviewed each round and every legitimate finding was addressed: hardened stripHtmlTags (tag-like trailing only), readSmallFile grow-guard (re-fstat), corrected the misleading visibleLength comment, stronger escapeRegExp/RICH_TAG_RE tests, and paths-ignore over a query-level exclude (which would have blinded product code).

🤖 Generated with Claude Code

…g, FS races)

The CodeQL self-gate (security-extended, tier:block) was RED on main, blocking
every PR. Fix all gated findings properly — no in-source suppression:

- js/incomplete-multi-character-sanitization (transmitter visibleLength,
  rich validateRichHtml): a `/<[^>]+>/g` tag-strip left a dangling, unterminated
  `<script` behind. New shared stripHtmlTags() removes a trailing tag-like
  fragment too, in one pass, while preserving a lone literal `<` in text.
- js/double-escaping (transmitter visibleLength, table.test): chained entity
  `.replace()`s could feed one decode into the next (`&amp;lt;`). New shared
  decodeHtmlEntities() consumes each entity exactly once via a lookup table.
- js/incomplete-sanitization (rich RICH_TAG_RE builder): the regex was built
  escaping only `-`. New shared escapeRegExp() escapes the full ES metachar set;
  install-skill's local escapeRegex is migrated onto it (DRY).
- js/file-system-race (install appendMarked + ensureSessionStartHook, telegram
  blobFor): replace check-then-read/-write TOCTOU pairs with atomic ops —
  readTextIfExists() (single read, catch ENOENT) and readSmallFile() (one fd:
  open -> fstat -> read -> re-fstat guard), backing up the in-memory snapshot
  rather than re-reading the path.
- js/file-access-to-http (scripts/*.ts, 10 findings): scope the gate to the
  shipped product surface via .github/codeql/codeql-config.yml paths-ignore:
  scripts — by-hand dev asset-generators, never shipped, no untrusted input.
  features/ keeps full coverage incl. this query.

New shared helpers live in features/render/html.ts (stripHtmlTags,
decodeHtmlEntities) and features/util/regex.ts (escapeRegExp), with unit tests
(html-sanitize, read-small-file) plus visibleLength integration cases.
bun test: 1031 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alex-mextner alex-mextner merged commit b35ea05 into main Jun 17, 2026
8 checks passed
@alex-mextner alex-mextner deleted the codeql-selfgate-fix branch June 17, 2026 11:47

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2158becd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// from that same in-memory copy. A separate existsSync()/readFileSync() per
// use would re-stat the path each time, racing with concurrent writers
// (js/file-system-race); a single read snapshots the bytes we then act on.
const original = readTextIfExists(settingsPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep unreadable settings from aborting install-skill

When ~/.claude/settings.json exists but cannot be read as a normal file (for example it is a directory, has bad permissions, or a transient read error occurs), readTextIfExists() rethrows before the existing parse/error guard runs. The previous implementation had the read inside the try and returned false, conservatively skipping the hook; this change now aborts install-skill after it may already have written other files. Move this read back under the guard or catch non-ENOENT errors here and return false.

Useful? React with 👍 / 👎.

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