Skip to content

fix: harden security audit findings (path traversal, approval gate, parser) - #6

Merged
nexx88 merged 7 commits into
masterfrom
chore/fix-sec
Aug 17, 2026
Merged

nexx88 merged 7 commits into
masterfrom
chore/fix-sec

Conversation

@nexx88

@nexx88 nexx88 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the 5 items surfaced by a whole-codebase security audit of yaas-triage. The audit found no high-confidence (>=8) exploitable vulnerabilities; these are the defense-in-depth / correctness hardening items below the exploitable bar. Each is a small, surgical change with tests kept green.

# Fix File
1 Reject path traversal from an unvalidated watch.json type before it builds/execs the checker path ([a-z0-9_]+ gate → misconfig verdict otherwise) tick.py
2 Apply the log-event.py quest_id traversal guard to slack-send.py so both send paths validate identically surfaces/slack-send.py
3 Drop pending_review -> start so the human-approval gate is structural, not dependent on the checker approval_state.py
4 Enforce header/TS adjacency in the Slack message parser (removes orphan/lone-TS mis-count) checkers/slack_utils.py
5 Remove dead _find_watch_json (unvalidated-path helper, unreferenced) ledger/approval-helper.py

Known residual (finding #4)

The Slack MCP returns messages as one flattened, unescaped text blob, so a body that forges an adjacent === header === + Message TS: pair remains indistinguishable from a real message boundary. The parser change enforces the format contract and closes the orphan/lone-TS path, but full closure needs structured (per-message) MCP output — documented inline with a ponytail: note. Impact is now further blunted by #3 (dispatch requires a structural review gate).

Test plan

  • 44/44 real suites pass; differential tick.py vs goldens 29/0 (no golden edits)
  • slack_utils, all approval-*, watch-guard, log-event, slack-send suites green
  • Functional: slack-send.py --quest-id ../../etcinvalid quest id exit 1; real ids pass through
  • Functional: wtype guard accepts every real watch type, rejects traversal/dotted/empty
  • Syntax + import checks on all 5 edited modules

(The one full-suite failure, path-references.test.sh, is entirely a local gitignored .ua/ plugin cache — 0 non-.ua refs — not code and not from this branch.)

🤖 Generated with Claude Code

nexx88 and others added 5 commits August 16, 2026 08:54
`wtype` is read straight from a quest's watch.json and used to build the
checker script path `checkers/{wtype}.py`, which is then executed. A value
containing `/` or `..` could escape the checkers/ dir and run an arbitrary
executable. Gate the path on `[a-z0-9_]+` (the charset of every real watch
type); anything else routes to the existing misconfig verdict instead of an
exec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
slack-send.py passed quest_id to quest_dir() without the traversal guard
that log-event.py already applies, so both send paths validated
inconsistently. Add the same guard (reject `/`, `.`, `..`, control chars)
once at the single origin so both quest_dir() call sites are covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`start` was legal from both `pending_review` and `reviewed`, so the
human-approval gate depended on the checker only marking reviewed items
dispatchable rather than on the state machine itself. Drop the
`pending_review -> start` transition so an unreviewed item can never move to
`executing`; the gate is now structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real Slack MCP message is `=== header ===` then `Message TS:` on adjacent
lines. The parser accepted any TS line as a message start, so a `Message TS:`
line an author typed into a body could be counted/mis-attributed. Require the
TS to be immediately preceded by a header, which enforces the format contract
and removes the orphan/lone-TS mis-count. A body that forges an adjacent
header+TS pair remains inherent to the MCP's unescaped text format (documented
inline); only structured MCP output closes that fully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unreferenced anywhere. It built a path from quest_id without the traversal
guard the live paths apply, so removing it also drops an unvalidated-path
helper that could be wired up unsafely later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@guangmian-circle guangmian-circle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving changes, adjusted one fix and adding tests to it to cover future regression.

@guangmian-circle

Copy link
Copy Markdown
Collaborator

Pushed the YAAS v2 docs snapshot onto this branch (97658a2): README rewrite, a Mode B block in CLAUDE.example.md, and a .gitignore entry for .git-yaas-v2/.

Three files, docs only, no source changes. The publish replaces the whole tracked tree from the mirror, so I first adopted this branch's security hardening into the mirror and verified the resulting tree is identical to chore/fix-sec apart from those three files. The approval gate, the quest_id path-traversal guard, and the header-adjacency parser fix are all intact. Publishing as a separate PR would have reverted them depending on merge order, which is why it landed here instead.

Local suite green: 46 suites, 29 differential goldens.

@guangmian-circle

Copy link
Copy Markdown
Collaborator

Rewrote the branch above 58d2b2b to clean up two bad commits of mine and land the docs.

The branch is now the five security commits unchanged, plus one signed commit (b2a115f, verified) carrying the security test coverage, the README/skills/ARCHITECTURE doc updates, and a .gitignore entry.

What it replaced:

  • c9030ca ("test: cover security hardening paths") was pushed unverified, authored as guangmian.kung+crcl@circle.com, an address not attached to a GitHub account. Its content is preserved in b2a115f, now correctly signed.
  • eb24a4d was a mistaken push of mine that re-parented the branch onto a4bba7c and dropped the five security commits from history. That is undone; 782b783, 045433c, 11cd9a5, 8acab5f and 58d2b2b are back with their original SHAs and authorship.

Every commit on the branch now reads verified=true, and the tree is unchanged from what was already reviewed apart from the docs.

@guangmian-circle
guangmian-circle self-requested a review August 17, 2026 02:01

@guangmian-circle guangmian-circle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving after making final changes based on the suggestions, as well as tightening up the README.

@guangmian-circle guangmian-circle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving once more, after correcting logo path.

@nexx88
nexx88 merged commit 0a2983e into master Aug 17, 2026
1 check passed
@nexx88
nexx88 deleted the chore/fix-sec branch August 17, 2026 02:54
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.

3 participants