Skip to content

fix(codeql): resolve 11 py/path-injection alerts via os.path.realpath+startswith - #492

Merged
liususan091219 merged 1 commit into
mainfrom
fix/codeql-path-injection-callsites
Apr 21, 2026
Merged

liususan091219 merged 1 commit into
mainfrom
fix/codeql-path-injection-callsites

Conversation

@liususan091219

Copy link
Copy Markdown
Collaborator

Summary

Closes the remaining 11 py/path-injection alerts by switching three flagged helpers to the CodeQL-recognized sanitizer pair.

Alert # File Line on current main
#17 #18 #19 src/agent-api.py 162, 163, 165 (call sites of _safe_path)
#23 #47 src/agent-api.py 630, 632 (inline /answer handler)
#46 src/agent-api.py 153 (inside _safe_path)
#28 #29 #30 #31 src/dashboard.py 465, 469, 488, 489 (call sites of _resolve_note_path)
#45 src/dashboard.py 50 (inside _resolve_note_path)

Root cause

Prior PRs (#487, #488) added Path(x).name and inline comments calling it a "CodeQL-recognized sanitizer". That turns out not to be true for the py/path-injection query. In codeql/python-all@7.0.4:

  • Path::PathNormalization::Range is only extended by os.path.normpath, os.path.abspath, and os.path.realpath (Stdlib.qll:1108-1132).
  • Path::SafeAccessCheck::Range is only extended by str.startswith (Stdlib.qll:5153).

Path.resolve(), Path.is_relative_to(), and Path.name are not modeled, so the helper's internal .name + .is_relative_to guards look like untracked pass-through flows to CodeQL — taint continues from raw_slug/task_id straight through to the file-system sinks.

Change

Three helpers switch to os.path.realpath(...) + str.startswith(base + os.sep):

  • src/agent-api.py::_safe_path
  • src/agent-api.py inline /answer task-writer
  • src/dashboard.py::_resolve_note_path

The regex whitelist at the front is preserved (defence-in-depth), so behaviour is identical for every input. Diff is +31 / -38.

Verification

Local CodeQL CLI 2.25.2 + codeql/python-queries@1.8.0:

$ codeql database create /tmp/codeql-sutando/db-fix --language=python --source-root=. --overwrite
$ codeql database analyze /tmp/codeql-sutando/db-fix .../PathInjection.ql --format=sarif-latest --output=/tmp/results.sarif
  • Before (current main at dc7bfbe): 11 path-injection alerts.
  • After (this branch): 0 path-injection alerts.

Test plan

  • python3 -c "import ast; ast.parse(open('src/agent-api.py').read())" — syntax OK
  • python3 -c "import ast; ast.parse(open('src/dashboard.py').read())" — syntax OK
  • Behavioural smoke: _safe_path correctly strips ../etc/passwd, /etc/passwd, .., and empty input — all remain inside base_dir or return None
  • Local CodeQL: 11 → 0 alerts
  • GitHub CodeQL workflow clean on this branch (will verify once checks run)

Supersedes #491 (comments-only; closed).

— Lucy (Mac Studio bot)

…19,23,28-31,45-47)

Resolves the remaining 11 py/path-injection alerts by switching _safe_path
(agent-api.py), _resolve_note_path (dashboard.py), and the /answer inline
handler to the CodeQL-recognized sanitizer pair:

  Path::PathNormalization : os.path.realpath
  Path::SafeAccessCheck   : str.startswith(base + os.sep)

Path.resolve / Path.is_relative_to are NOT in codeql/python-all's
PathInjection customizations (Stdlib.qll only models os.path.normpath,
abspath, realpath and str.startswith), which is why prior refactors
#487/#488 didn't close the alerts.

Verified locally with CodeQL CLI 2.25.2 + codeql/python-queries 1.8.0:
  before: 11 path-injection alerts (current main at dc7bfbe)
  after:  0 path-injection alerts

Behavioral sanity checks unchanged: traversal, absolute, empty, and
dotfile inputs are all stripped by the existing regex whitelist and
then confined by the realpath + startswith guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liususan091219
liususan091219 merged commit 0a4823e into main Apr 21, 2026
1 check passed
@liususan091219
liususan091219 deleted the fix/codeql-path-injection-callsites branch April 21, 2026 03:18
liususan091219 added a commit that referenced this pull request Apr 21, 2026
…-logging) (#493)

## #43 js/resource-exhaustion (src/web-client.ts:2549)

#489's `Math.min(ttl, MAX_TTL_MS)` is an `isNumericFlowStep` (numeric
passthrough) in `codeql/javascript-all@2.6.27` — it narrows the value but
doesn't register as a sanitizer. The `js/resource-exhaustion` config only
blocks taint via `UpperBoundsCheckSanitizerGuard`, which matches
`RelationalComparison` AST nodes (`<`, `<=`, `>`, `>=`).

Rewrote the clamp as `ttl <= MAX_TTL_MS ? ttl : MAX_TTL_MS` — semantically
identical, but CodeQL now sees `ttl` as the lesser operand in a `<=`
comparison and blocks taint in the true branch.

## #44 js/clear-text-logging (src/voice-agent.ts:71)

The `${value.length}` and `${prefixOk}` interpolations in `console.error`
both read properties of `value`. The env var `GEMINI_API_KEY` triggers
`HeuristicNames` → tainted source; `allowImplicitRead` in the cleartext
config pulls any property of a tainted value into the logger sink.
#486 stopped echoing substrings but the length + ternary-prefix diagnostic
still flow the taint.

Dropped those two interpolations from the log; kept the name, expected
format ("AIza..." 35-60 chars), and remediation URL. Diagnostic loss is
minor — a malformed key always needs to be regenerated anyway.

## Verification

Local CodeQL CLI 2.25.2 + codeql/javascript-queries@2.3.7:
- main (dc7bfbe):  2 alerts (resource-exhaustion @ web-client.ts:2549, clear-text-logging @ voice-agent.ts:71)
- this branch:     0 alerts

`npx tsc --noEmit` clean.

Closes #43, #44. Together with #492 (merged earlier this session), this
zeroes the CodeQL security-code-scanning queue.

Co-authored-by: Xueqing Liu <xueqingliu@Xueqings-Mac-Studio.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 21, 2026
…494)

* fix(discord-bridge): allowlist-gate [file:/send:/attach:] sends

The result-text `[file: PATH]` / `[send: PATH]` / `[attach: PATH]` markers
previously passed a bare `os.path.isfile()` check before attaching. Any
path that existed on disk would be forwarded to Discord — trusted today
because only agent output produces those markers, but brittle against
future ingress changes or a tooling drift that starts preserving
structured file items.

This change:
- Adds `_is_path_sendable(path)` using os.path.realpath + prefix match
  (matches the sanitizer shape CodeQL recognized in #492).
- Allows paths under `results/`, `notes/`, `~/Desktop/iclr-backups/`, or
  with `/tmp/sutando-` / `/private/tmp/sutando-` prefix.
- Fails closed with `(file not allowed: PATH)` visible inline.
- Patches both sinks: normal reply path (line 757) + proactive DM path
  (line 857).

Tested via 6-case helper unit test (repo-local allowed, /tmp/sutando-
allowed, /etc/passwd rejected, non-existent rejected, random /tmp
rejected, notes/ allowed).

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

* fix(discord-bridge): widen allowlist per MacBook review

MacBook cold-review flagged 3 legitimate agent-generated paths that
would have been rejected under the initial narrow scope:

- docs/ — repo-local; historical send of stand-avatar-candidate-1.png
  found in results/ archive.
- /tmp/echo-intro/*.mp4 — Echo Act IV launch video assets.
- ~/Documents/sutando-launch-assets/* — persistent copy of launch
  assets Chi saved yesterday.

All 3 are known agent-generated roots (not attacker-controlled), so
the design intent of "trust agent output under known roots" covers
them. Widening preserves the fail-closed guarantee for unknown paths.

6/3 new-case sanity pass (docs/, /tmp/echo-, /etc/passwd).

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

---------

Co-authored-by: Chi <wangchi@Chis-Mac-mini.hsd1.wa.comcast.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 added a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…chi#17-19,23,28-31,45-47) (sonichi#492)

Resolves the remaining 11 py/path-injection alerts by switching _safe_path
(agent-api.py), _resolve_note_path (dashboard.py), and the /answer inline
handler to the CodeQL-recognized sanitizer pair:

  Path::PathNormalization : os.path.realpath
  Path::SafeAccessCheck   : str.startswith(base + os.sep)

Path.resolve / Path.is_relative_to are NOT in codeql/python-all's
PathInjection customizations (Stdlib.qll only models os.path.normpath,
abspath, realpath and str.startswith), which is why prior refactors
sonichi#487/sonichi#488 didn't close the alerts.

Verified locally with CodeQL CLI 2.25.2 + codeql/python-queries 1.8.0:
  before: 11 path-injection alerts (current main at dc7bfbe)
  after:  0 path-injection alerts

Behavioral sanity checks unchanged: traversal, absolute, empty, and
dotfile inputs are all stripped by the existing regex whitelist and
then confined by the realpath + startswith guard.

Co-authored-by: Xueqing Liu <xueqingliu@Xueqings-Mac-Studio.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 added a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
… (clear-text-logging) (sonichi#493)

## sonichi#43 js/resource-exhaustion (src/web-client.ts:2549)

sonichi#489's `Math.min(ttl, MAX_TTL_MS)` is an `isNumericFlowStep` (numeric
passthrough) in `codeql/javascript-all@2.6.27` — it narrows the value but
doesn't register as a sanitizer. The `js/resource-exhaustion` config only
blocks taint via `UpperBoundsCheckSanitizerGuard`, which matches
`RelationalComparison` AST nodes (`<`, `<=`, `>`, `>=`).

Rewrote the clamp as `ttl <= MAX_TTL_MS ? ttl : MAX_TTL_MS` — semantically
identical, but CodeQL now sees `ttl` as the lesser operand in a `<=`
comparison and blocks taint in the true branch.

## sonichi#44 js/clear-text-logging (src/voice-agent.ts:71)

The `${value.length}` and `${prefixOk}` interpolations in `console.error`
both read properties of `value`. The env var `GEMINI_API_KEY` triggers
`HeuristicNames` → tainted source; `allowImplicitRead` in the cleartext
config pulls any property of a tainted value into the logger sink.
sonichi#486 stopped echoing substrings but the length + ternary-prefix diagnostic
still flow the taint.

Dropped those two interpolations from the log; kept the name, expected
format ("AIza..." 35-60 chars), and remediation URL. Diagnostic loss is
minor — a malformed key always needs to be regenerated anyway.

## Verification

Local CodeQL CLI 2.25.2 + codeql/javascript-queries@2.3.7:
- main (dc7bfbe):  2 alerts (resource-exhaustion @ web-client.ts:2549, clear-text-logging @ voice-agent.ts:71)
- this branch:     0 alerts

`npx tsc --noEmit` clean.

Closes sonichi#43, sonichi#44. Together with sonichi#492 (merged earlier this session), this
zeroes the CodeQL security-code-scanning queue.

Co-authored-by: Xueqing Liu <xueqingliu@Xueqings-Mac-Studio.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…onichi#494)

* fix(discord-bridge): allowlist-gate [file:/send:/attach:] sends

The result-text `[file: PATH]` / `[send: PATH]` / `[attach: PATH]` markers
previously passed a bare `os.path.isfile()` check before attaching. Any
path that existed on disk would be forwarded to Discord — trusted today
because only agent output produces those markers, but brittle against
future ingress changes or a tooling drift that starts preserving
structured file items.

This change:
- Adds `_is_path_sendable(path)` using os.path.realpath + prefix match
  (matches the sanitizer shape CodeQL recognized in sonichi#492).
- Allows paths under `results/`, `notes/`, `~/Desktop/iclr-backups/`, or
  with `/tmp/sutando-` / `/private/tmp/sutando-` prefix.
- Fails closed with `(file not allowed: PATH)` visible inline.
- Patches both sinks: normal reply path (line 757) + proactive DM path
  (line 857).

Tested via 6-case helper unit test (repo-local allowed, /tmp/sutando-
allowed, /etc/passwd rejected, non-existent rejected, random /tmp
rejected, notes/ allowed).

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

* fix(discord-bridge): widen allowlist per MacBook review

MacBook cold-review flagged 3 legitimate agent-generated paths that
would have been rejected under the initial narrow scope:

- docs/ — repo-local; historical send of stand-avatar-candidate-1.png
  found in results/ archive.
- /tmp/echo-intro/*.mp4 — Echo Act IV launch video assets.
- ~/Documents/sutando-launch-assets/* — persistent copy of launch
  assets Chi saved yesterday.

All 3 are known agent-generated roots (not attacker-controlled), so
the design intent of "trust agent output under known roots" covers
them. Widening preserves the fail-closed guarantee for unknown paths.

6/3 new-case sanity pass (docs/, /tmp/echo-, /etc/passwd).

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

---------

Co-authored-by: Chi <wangchi@Chis-Mac-mini.hsd1.wa.comcast.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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