Skip to content

fix(agent): clear CodeQL py/path-injection on the #1083 result-callback sink#1275

Open
AndriiPasternak31 wants to merge 1 commit into
devfrom
AndriiPasternak31/fix-1083-codeql-path-sink
Open

fix(agent): clear CodeQL py/path-injection on the #1083 result-callback sink#1275
AndriiPasternak31 wants to merge 1 commit into
devfrom
AndriiPasternak31/fix-1083-codeql-path-sink

Conversation

@AndriiPasternak31

Copy link
Copy Markdown
Contributor

Why

PR #1273 (#1083 fire-and-forget dispatch) squash-merged into dev while CodeQL was still failing — CodeQL is not a required check, so auto-merge landed it. The merged code in docker/base-image/agent_server/services/result_callback.py trips py/path-injection (3 high) at the pending-result write/unlink sinks: the agent-supplied execution_id flows into ~/.trinity/pending-results/<id>.json.

This is a follow-up that lands the correct, CodeQL-clean fix on dev.

What

Inline the #950 path-containment guard (os.path.normpath + startswith prefix-check) co-located with each write/replace/unlink sink in _persist/_delete, and drop the now-unused _pending_path helper.

Why this shape (the journey)

The guard had to be at the sink, in the same function:

Attempt Result
regex allowlist in try_spawn_async (caller) CodeQL ignores caller-side guards
resolve() + is_relative_to() in _pending_path not recognized as a barrier
os.path.basename() in _pending_path not recognized as a barrier
normpath + startswith in _pending_path cleared the construction site, but re-flagged the _persist/_delete sinks (barrier not propagated across a callee return)
normpath + startswith inlined at the sink (this PR) matches #950's proven co-located guard+sink shape

Defense-in-depth is layered: the try_spawn_async regex belt still rejects non-token/UUID ids upstream (→ sync fallback); the inlined guard is the suspenders. The value is backend-generated (secrets.token_urlsafe/UUID) and never attacker-controlled in normal operation — these were defense-in-depth alerts, not an exploitable path.

Tests

tests/unit/test_1083_result_callback.py: test_persist_delete_reject_traversal (traversal id is best-effort rejected — nothing written outside _PENDING_DIR, no raise) + existing persist/resend/delivery coverage. 27 passed locally; ruff clean.

Refs #1083

🤖 Generated with Claude Code

#1083, CodeQL)

The #950 normpath+startswith guard cleared the construction site but CodeQL
re-flagged the _persist/_delete sinks: it treats _pending_path's arg→return as
tainted and does not propagate a CWE-022 barrier across a callee return. Inline
the guard co-located with each write/replace/unlink sink (exactly how #950's
guard sits with its rmtree/exists sink), and drop the now-unused _pending_path.

Refs #1083

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
if dest != base and not dest.startswith(base + os.sep):
raise ValueError(f"pending-result path escapes {base}: {execution_id!r}")
tmp = f"{dest}.tmp"
Path(tmp).write_text(json.dumps(record))
raise ValueError(f"pending-result path escapes {base}: {execution_id!r}")
tmp = f"{dest}.tmp"
Path(tmp).write_text(json.dumps(record))
Path(tmp).replace(dest)
raise ValueError(f"pending-result path escapes {base}: {execution_id!r}")
tmp = f"{dest}.tmp"
Path(tmp).write_text(json.dumps(record))
Path(tmp).replace(dest)
dest = os.path.normpath(os.path.join(base, f"{execution_id}.json"))
if dest != base and not dest.startswith(base + os.sep):
return
Path(dest).unlink(missing_ok=True)
@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

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