Skip to content

fix: 77 validated findings from the 2026-10-13 bug hunt - #322

Merged
jkyberneees merged 122 commits into
mainfrom
fix/bughunt-2026-10-13
Oct 14, 2026
Merged

jkyberneees merged 122 commits into
mainfrom
fix/bughunt-2026-10-13

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes 77 findings from the 2026-10-13 bug hunt over v2.34.7 (7 high, 44 medium, 26 low). Each finding has a TestRED_* regression test (redhunt_2026_10_13_*_test.go). The fixes went through per-area Sonnet reviews and two Opus full-branch reviews.

High severity

  • A sub-agent's risk cap now carries over to its own sub-agents, and allowlist entries can't exceed any cap that applies to the child.
  • Shell classifier bypasses closed: file-fed xargs/parallel, curl -K and fd -x in short-flag clusters, curl --variable %SECRET, declare -n aliases of secret variables, and unclassified find -exec inner commands.

Other areas: danger/guard, redaction, config, loop, memory, Telegram, serve, tools, runtime/MCP and CLI. The commit list has the per-finding details.

Behavior changes

  • The classifier is stricter on:
    • bundle exec, rails, tox and yarn <bin>
    • file-fed xargs with commands that aren't read-only
    • package publishing, and npm config set script-shell
  • git pull && ./build.sh asks again before running a script that was read earlier.
  • MCP tool names whose server ends in _ or whose tool starts with _ now get a hash suffix (see docs/MCP.md).
  • POST /api/prompt with an unknown session_id returns 404.
  • REPL history uses a new file format. Older files still load as they are.

Tests

  • go build, go vet, gofmt and golangci-lint are clean.
  • go test -short -race passes on every touched package. internal/danger and internal/guard were also run with HOME=/home/user.
  • The four monotonicity fuzz targets ran for 30s each and passed.
  • Classifier verdicts were compared between main and this branch over about 19.7k strings. None is less strict, except two parallel options that main denied by accident; they now prompt.

Coverage

No touched package dropped. cmd/odek went from 77.4% to 77.7%, and internal/agent from 83.6% to 85.1%.

🤖 Generated with Claude Code

jkyberneees and others added 30 commits October 14, 2026 08:29
…bodies

argv0 returned a leading '(' word or a whole $( ) / backtick substitution verbatim,
leaking assignment values into the event stream. Words carrying shell
metacharacters now yield an empty program name. Adds the red-hunt tests for this area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion and plan titles

Byte slicing at fixed offsets split multibyte characters and sent U+FFFD to the
model. Cuts now use cutUTF8 and advance the tail to the next rune start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cache breakpoints

A resumed run inserted a fresh memory block without removing the one persisted
by the previous run, so copies and cache breakpoints accumulated past
Anthropic's limit of 4. Persisted memory blocks are now dropped before the
current one is injected, and only the last cache-marked later system block
keeps its breakpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… user turn on Anthropic

System messages hoisted into the top-level system field left the superseded
draft as the final message, an assistant prefill. Trailing system messages
after the last assistant turn now go out as one trailing user message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metacharacter rejection blanked ordinary commands such as 'ls;rm x' or
'cat<f'. The name now ends at the first unquoted operator; a prefix opening a
subshell, group or substitution still yields an empty name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erator

'K=v;curl x' reported a later argument as the program name. The name is now
unknown when the assignment word contains an unquoted operator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rompt

A pending clarify waited on a channel closed only when the turn returned, and the approver was never cancelled, so cancelling the turn left it blocked for minutes. The run context now cancels the approver and releases clarify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A case-variant spelling of ~/.odek/media skipped the per-chat scope check on case-insensitive filesystems. Containment is now compared case-folded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ResumeSession reuses the canonical tg-<chat> ledger key but kept the replaced conversation's reads; it now forgets them. Includes the case-fold helper test and docs for the telegram fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/new locked the chat mutex inline, stalling every chat behind a running turn. Resets now cancel the turn and run on a goroutine; later turns wait on a per-chat reset gate so they land in the new session. /resume also cancels the turn, drops background jobs and forgets the read ledger after the turn unwinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cancel branch left the Approve/Deny keyboard live; it now edits the prompt as the timeout branch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folding everywhere made media/CHAT<id> count as the chat's own subdirectory on Linux. The fold decision is now a platform variable; helper tests cover both modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e values

The run tail recorded full tool output per frame (count-bounded only), pinning
hundreds of MiB per run. Frames are now clipped (8 KiB head+tail per value) and
the tail is capped at 4 MiB, dropping oldest events first. All RED tests for
the serve area live in the shared redhunt file added here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e throttled lookup path

An unknown session_id was accepted with 202 and a full agent was built before
the turn failed. It now returns 404 via rejectSessionLookup before any build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

Large values overflowed int64 nanoseconds and wrapped to a sub-second timeout.
The integer seconds are compared against the 600s cap first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validation failures after PromptsStarted (unknown @-session, attachments,
expanded-prompt cap, ...) returned without bumping prompts_failed. The failure
is now counted once in the turn's deferred outcome block for any non-completed
started prompt, replacing the two ad hoc increments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is rejected

A 503/500 after store.Create left an orphan 'Uploaded files' session whose id
and token were never returned. The new session is now removed on every
non-success path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pong came from the connect-time snapshot and kept the original model after
a per-prompt switch, so the health popover reverted every 20s. It now reads the
lock-protected connection model. The existing pong test pinned the stale value
and now accepts the connect-time or switched model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview returned three zero times for a cron like 0 0 30 2 *, while saving
the same schedule is refused. It now returns 400 as Job.Validate does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
requireLocalHost compared the raw [::1] to ::1 when SplitHostPort failed and
returned 403. Brackets are now stripped in that case, matching the origin check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refusal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview tests

Trimming used utf8.ValidString over the whole half, so a single invalid byte
dropped most of the head or tail. Only the partial rune at each cut is trimmed
now. Adds tests for multibyte and invalid-byte clipping, nested and
unmarshalable values, and the never-matching cron preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sub-agent's delegate_tasks envelope carried only the grandchild's own declared max_risk/profile, so a capped child could delegate past its cap. The envelope now stamps parent_max_risk and the child clamps to it. (Shared RED test file also holds the audit bare-IP test.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ResourcesIn only knew URLs, paths and name.tld tokens, so nc/ssh/curl to a bare IP after untrusted ingest was never flagged. IPv4 and bracketed IPv6 literals are now resources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
net/http compares host names only, so the token_env token followed an https-to-http downgrade and cross-port redirects. The redirect guard now removes Authorization (with Mcp-Session-Id) unless scheme, host and port match the configured URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(gh_, issues) and (gh, _issues) both registered gh___issues, panicking the registry or shadowing a tool. Ambiguous joins now always carry the hash suffix and loadMCPTools errors on any remaining duplicate registered name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn and missing caps

Allowlist entries won over class denials, so a grandchild picking an allowlisting profile escaped the cap; they are now dropped when the cap denies them. A fully locked-down sub-agent stamps cap "none" (deny all), a depth>=2 task file without parent_max_risk is refused, and tests drive subagentCmd itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
https://h and https://h:443 (http/:80) are the same origin, so the token is no longer dropped on a same-origin redirect that spells the default port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xed names

The duplicate check is a small helper with its own test; docs note that allow-list entries for ambiguous names need the hash suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jkyberneees and others added 21 commits October 14, 2026 09:49
Classifying each nested find -exec payload in full was quadratic in chain
depth. The walk now descends to the innermost action commands, examines
each once, and fails closed (unknown) past 64 of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rted as hidden

Flagging every control byte as a hidden character rejected ordinary
terminal output and MCP schema text. NormalizeForScan still removes them so
a control byte inside a keyword cannot hide a phrase; only invisible
format characters are reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ar/jar/pax/dpkg-deb extraction, hg/svn updates, submodule/sparse-checkout/
worktree verbs, ed/ex, and directory-merging cp/mv/rsync (globs, trailing
slash, recursive) now void the licence of later scripts in the command.
Read-only variants keep it: stash list/show, merge --abort, clean -n, a new
branch at HEAD, reset --soft/--mixed, restore --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-Bm/-um clusters, versioned interpreter names (python3.11) and a package's
__init__.py/__main__.py (and parent packages) are gated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
COOKIE, WEBHOOK and AUTH_SOCK names are secret-shaped, and a nameref target
built from an expansion is treated as secret (fail closed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olds regional indicators

0x1C-0x1F and CSI sequences read as a space or as nothing, so the scan
matches both readings; regional-indicator letters fold to ASCII. Adds guard
tests (coverage 84.9%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… package-manager shell settings, ext:: remotes

xargs/parallel options are read with the option grammar (-0a, -ta, bare
parallel -a, shell -c with no payload); with fully dynamic argv only a
read-only allowlist stays safe. yarn/pnpm/bun/nvm/fnm/pyenv/rbenv runners and
'bundle e' classify the command they run. More start-time files are
persistence (IPython profiles, *rc.d, .pdbrc, .irbrc, Jupyter config, Emacs
init, vim/vimrc, .vscode/tasks.json). npm/yarn/pnpm config set of
script-shell/node-options is persistence and the flags on a run are
code_execution. git ext::/fd:: remotes are code_execution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Options after -m NAME are the module's argv, not interpreter options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icence

checkout -f -b / -B / switch -C / --discard-changes discard local edits and
merge --abort restores files; only a plain new branch at HEAD stays
read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… space and as nothing

OSC/DCS/APC/PM/SOS strings, two-byte escapes, U+009B and every C0/C1
control between words are read both ways (OSC payload text stays visible
in the space reading).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-manager set keys, dynamic-argv allowlist, yarn scripts

url.ext::/url.fd:: config and protocol.*.allow=always are code_execution;
parallel's value-taking options no longer hide the command; every word after
a package-manager set is checked as a key; sed/sort/uniq/file leave the
dynamic-argv allowlist; bare 'yarn WORD' matches npm run/pnpm and yarn's
read-only builtins are recognised. Docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n JSON-encoded text

In plain shell text a double quote or a literal \t / \n inside a password
ended redaction early. The command tokenizer and operand scanner now apply
JSON string rules only when the command sits inside a JSON string (odd
unescaped quote count on its line, tracked incrementally); plain text
redacts to the next blank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ving

Lines above the limit are discarded while the rest loads; only a real read error disables persistence, with a one-line warning on stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hild

Filtering against the final config also stripped allowlisted unknown-class commands from trusted, uncapped children. Entries are now dropped only when the trust lockdown, task max_risk or parent cap denies their class; operator defaults and the operator profile envelope keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run of ESC P / ESC ] introducers was rescanned from every position, making
ScanInjection quadratic; an unterminated string now runs to the end of input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ec end, runner options, dry-run publish, hook files as write targets, AUTH_SOCK

parallel options come from GNU parallel's documented list with exact names
(boolean options no longer swallow the command, unknown options never do);
dynamic argv uses the command's worst-case class (readers safe, local writers
local_write, curl/wget upload) and options/placeholders are not fixed
arguments; find -exec ends only at ; or {} +; nvm/bun options before the
command are skipped; publish --dry-run is not an upload; start-time hook
files are persistence only as write targets; SSH_AUTH_SOCK is not secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An option missing from the table was read as a flag, so a value it takes
would become the command and hide the real one. Unknown long or short options
now classify the whole parallel command as unknown; listed options are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread internal/redact/redact.go Fixed
Comment thread internal/redact/redact.go Fixed
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek d97454a Commit Preview URL

Branch Preview URL
Oct 14 2026, 09:34 AM

jkyberneees and others added 5 commits October 14, 2026 11:23
…lt thresholds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… at a hostname boundary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecting JSON context

A " inside a single-quoted shell span flipped the tracker into JSON mode,
so a later literal \t or \n in a password cut redaction short. Single-quoted
spans outside JSON strings are now skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing wins; patch/uniq/split write targets

Program-valued parallel options (--compress-program, --decompress-program,
--rpl, -S/--sshlogin) run their value (judged as a command, floor
code_execution) and path-valued ones (--results, --joblog, --return,
--basefile, --tmpdir, --workdir, --transferfile) are write targets; a
sensitive or system target stays unknown as before. A later false dry-run
setting makes a publish an upload again. The file patch rewrites, patch -o,
and the output operand of uniq/split count as start-time hook write targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit 53fba6d into main Oct 14, 2026
10 checks passed
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