Skip to content

fix: 80 validated findings from the 2026-10-12 bug hunt - #317

Merged
jkyberneees merged 125 commits into
mainfrom
fix/bughunt-2026-10-12
Oct 12, 2026
Merged

jkyberneees merged 125 commits into
mainfrom
fix/bughunt-2026-10-12

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Fixes 80 validated findings from the 2026-10-12 functional + security bug hunt over v2.34.2 (ce0bbfef): 18 high, 42 medium, 20 low, none rejected.

Process per area (danger, loop, memory, telegram, serve, tools, config, runtime, ops): an Opus hunter proved each finding with a failing TestRED_* test; a Sonnet fixer re-validated it on the unmodified base, then RED → GREEN → coverage → docs, one commit per finding (a few related findings share a commit); a Sonnet adversarial review per area, with a second fix round where it found gaps. The nine branches were cherry-picked together, then two full-branch Opus adversarial reviews (4 + 7 issues) and two Sonnet re-reviews of those fixes ran, each with its findings fixed.

High-severity highlights

  • danger — let 'x[$(rm -rf ~)]=1' and other arithmetic/subscript substitutions auto-allowed; env dumps (jq -n env, ps eww, awk ENVIRON, export -p) classified safe; gpg --photo-viewer / --exec-path code execution classified safe.
  • tools — SSRF via fullwidth/IDN hosts (127.0.0.1) in browser/http_request (UTS-46 mapping now inside danger.ClassifyURL, shared by tools and the batch card); APFS/NTFS case-fold aliases (~/.odek/ſecrets.env, ~/.ſsh) bypassing trust-anchor and home-credential protection (danger.FoldPathCase).
  • loop — batch approval card classified diff by path only while the tool read path_a/path_b, hiding a /etc/master.passwd read behind batch trust.
  • config — a project ./odek.json could add url/token_env to an operator auto-approved MCP server and send the operator token to an attacker URL; sub-agent artifacts readable across sessions; sub-agents of a sandboxed parent ran unsandboxed; project config ${VAR} expansion of host env.
  • memory — Telegram session_search could read other chats' and the operator's sessions.
  • serve — @-references loaded ~/.odek/secrets.env, SSH keys and .env into the prompt with no approval (now refused by danger.IsSensitiveReadPath; search filtered the same way).
  • telegram — one "Trust Session" tap auto-approved all later media uploads; scheduled-run and /plan_resume content entered chat sessions without taint; MEDIA: path parsing and FallbackTransport body replay.

Medium/low fixes span MCP stdio response caps and dead-client fail-fast, Anthropic thinking-block persistence (redaction-safe, provider-scoped), redaction gaps (bare token/secret/pass/pwd keys with a credential-shape check, netrc/XML/arrow forms, non-Bearer Authorization, Docker auth) without redacting ordinary code, untrusted-tag look-alike neutralising (linear), audit divergence and terminal escaping, REST approval double-answer, rate limits, run-lock files and the stale-inode race, MarkdownV2 delivery failures, schedule timezone, data races.

Per-finding detail: .claude/bughunt-2026-10-12/ (gitignored) — fix-input-*.json, fix-reports/, review-reports/.

Coverage (before → after)

package before after
cmd/odek 75.3% 76.6%
internal/danger 92.4% 92.7%
internal/redact 94.8% 96.8%
internal/agent 72.1% 82.3%
internal/mcpclient 92.2% 96.2%
internal/llmclient 84.5% 86.4%
internal/config 86.6% 86.9%
internal/memory 85.0% 85.1%
internal/telegram 91.0% 91.1%

Test plan

  • go build ./..., go vet ./..., gofmt -l, go mod tidy -diff clean
  • go test ./... (all packages)
  • go test -race on every changed package
  • HOME=/home/user go test -race ./internal/danger/
  • Fuzz: FuzzSeparatorThenWipe, FuzzPipeIntoShell, FuzzHarmlessPrefixKeepsRank, FuzzAnalyzeBounded 30s each
  • CI: golangci-lint (staticcheck) and govulncheck (not runnable locally on Go 1.27; new indirect dep golang.org/x/text via golang.org/x/net/idna)

Known residuals (documented, not fixed)

  • v='a[$(cmd)]'; let "$v" (substitution held in a variable); git $x with a truly dynamic verb; standalone PROMPT_COMMAND+= classifies unknown (deny) rather than destructive.
  • Hardlinks to protected files inside the workspace are not detected by the @-reference gate.
  • UUID / dash-grouped values after bare token= stay visible; legacy sessions with a thinking signature but no recorded provider replay as before.

🤖 Generated with Claude Code

jkyberneees and others added 30 commits October 12, 2026 12:12
… tools do

The card classified diff by its path argument only (path_a/path_b take
precedence) and http_request/browser by method/action only, so a decoy path or an
internal URL stayed off the card and batch trust auto-approved the real prompt.
Now diff takes the worst of the paths it reads, http_request/browser navigate
include the URL class, and browser click/back/snapshot withhold batch trust.
Covers loop-1 and loop-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A replacement check was installed without bumping the epoch, so a sibling call
that ran earlier in the same batch was recorded as its passing evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shellWords split on whitespace inside $(...), ${...} and backtick assignment
values, so a fragment of the value was reported as the program name. Those
substitutions now count as part of one word; unterminated ones are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bg_start runs shell commands but was ignored by recordMutation, so a reply
denying all actions after a background rm/make got no consistency notice.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quotes and parentheses inside a $( ... ) substitution (and double-quoted
substitutions) still split the word, leaking a value fragment as argv0. A
substitution is now consumed as a unit with nested quotes, parens and
substitutions; an unterminated one drops the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
snapshot fetches nothing, so it no longer withholds batch trust; click and
back stay unclassifiable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A provider STT backend uploads the audio but the card showed only the local
path class; it now takes the worse of path class and network_egress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bufio.Scanner raises its limit to the initial buffer capacity, so a
max_response_bytes below 64 KiB was silently not enforced on stdio. The initial
buffer is now bounded by the configured cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After readLoop returned (oversized line or server exit) later calls registered
a waiter nobody routed and blocked for the full server timeout. readLoop now
records a sticky error, call() returns it immediately, and an oversized line
closes stdin and kills the server as documented.

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

Trailing text items were appended after the capped envelope and the whole string
was cut from the end, dropping the artifact metadata lines. Trailing text is now
folded into the envelope text so the single capping pass shrinks text first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Duplicate names became duplicate registry entries and panicked tool.NewRegistry
at agent construction. Discover now fails closed on a repeated name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pattern up to 512 runes was kept verbatim, letting a server place a sentence
of instructions outside the untrusted boundary. Patterns with line breaks,
control characters or more than three spaces now move to the wrapped docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A FIFO named AGENTS.md blocked os.ReadFile forever and hung every agent
construction in that directory. LoadProjectFile now requires a regular file,
opens non-blocking, re-checks the handle and reads at most the size cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mapResult kept only the joined reasoning text and the last signature, so
redacted_thinking blocks were lost and multi-block turns replayed text that no
longer matched its signature. Blocks are now carried through CallResult into
session.Message (persisted, redacted, cloned) with the signing provider, and
replayed verbatim only to the same API format; a cross-provider continue drops
them. Covers both the redacted-block and the multi-block findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save redacted ThinkingBlock text but kept the signature, so a block that held a
secret replayed as altered text with the original signature and the provider
rejected every continue. The turn's blocks, signature and provider are now
dropped when redaction changes the signed text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spaceless or escape-separated prose passed the space-count check. A kept pattern
is now at most 128 runes with at most two runs of six or more letters, counting
escapes such as x20 and s as separators; structural regexes stay.

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

HandleResponse reported a match for any id still pending and silently dropped a second answer, so the REST bridge acked a deny after the command had been approved. Delivery now removes the pending entry; later answers report not found. Existing HandleResponse tests that re-read the pending map after delivery now hold the channel.

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

handlePrompt acquired the per-session lock (creating a lock file) before loading or authenticating the session, so every unknown id left a permanent lock file. The session is now loaded and token-checked first and again under the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bels stay valid UTF-8

shorten sliced bytes, so a multi-byte rune straddling the limit left invalid UTF-8 in new session titles and run labels. It now cuts on a rune boundary.

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

Tool names (model-supplied) and the renamable title were embedded raw in headings and metadata lines, so a newline forged document structure. They now pass through SanitizeInline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE/POST /api/sessions/{id}, /api/cancel and the jobs/artifacts/uploads/subagents endpoints answered 404 vs 401 without the per-IP limiter that GET and recovery use, an unthrottled session-id oracle. All rejections now share the lookup limiter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without an index, Store.List loaded each full session and cleared only Messages, so listings leaked every session's memory buffer and decision receipts to instance-token holders. The fallback now builds rows from the same fields as the index path.

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

With the workspace at $HOME, @.odek/secrets.env, @.odek/sessions/... or @.ssh/id_rsa expanded straight into the prompt with no approval. FileResolver.Load now refuses any target classified system_write or higher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Search listed names and sizes under ~/.odek, credential directories and other system_write targets when the workspace is $HOME. Matches now pass the same classifier gate as Load. Adds a long-s alias test that skips until the classifier case-fold fix is merged.

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

startServeRun's invalid-token 401 for a known session id was unthrottled. It now goes through the same limiter as the other session-scoped endpoints.

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

mcpExecEqual ignored url/token_env for command servers, so a project could
redirect an auto-approved stdio server to a remote URL with the operator's
token. Every branch now compares command, args, env, url, token_env, limits
and roots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es before the pillar

Blockquote and list-prefixed headings escaped imitation stripping, and an
unterminated code fence made the rest of the identity opaque and swallowed
the appended pillar. Quote markers are now peeled for detection, an unclosed
opener no longer shields content, and any open fence is closed before the
authoritative pillar is appended.

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

A cloned repo could set max_concurrency to any value and lift the delegate
limiter past the operator ceiling. Project values are clamped to the global
value (default 3) and the fallback limiter is capped at 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
promote keyed the registry by directory name while the loader looks it up by
the skill's frontmatter name, so a skill whose directory differs from its name
stayed NeedsReview after a successful promote. Record under the loaded name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jkyberneees and others added 25 commits October 12, 2026 12:42
printf -v'a[$(cmd)]' x hid the NAME inside the fused option word; the spaced form
was already classified. Fused -vNAME is now read the same way.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…vironment dumps

awk/gawk/mawk/nawk ENVIRON and PROCINFO, yq env()/strenv() and readonly -p with no
operand disclose the environment like env. jq's lowercase $env is an ordinary
variable and stays safe.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ts as broadcast

[a-z], \\w, [[:alnum:]] match every process name like '.', and -u root / -U 0 signal
every system process; all are system_west. An uncompilable pattern stays
system_write (prompt) rather than unknown (deny).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e non-default homes

--sign matched as an abbreviation of --sign-key and became system_write. Complete
options now win. An explicit --homedir or GNUPGHOME other than ~/.gnupg reads
that directory's gpg.conf, which can name a photo viewer, so it is code_execution.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…gawk -E

PROMPT_COMMAND, PS0/PS1/PS2/PS4 substitutions and BASH_FUNC_name%% bodies given as
assignments run as code in the shell the command starts, so their values are
classified as commands. gawk -E/--exec FILE is gated like -f. GNUPGHOME=DIR is
checked like --homedir.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rompts

These render blank or invisible but are printable by category.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…E in fuzz targets

Helpers are now rhAtLeast/rhSafe. The monotonicity fuzz targets set a real
non-root HOME (a real directory, since /home is an automount point on macOS and
made every lookup slow) so they pass as root and in CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…p additions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refuse trust anchors, home credential dirs, rc files, credential-shaped names and system secrets; ordinary files under /opt, /srv, /tmp load again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batch card and the tools now share one classifier; unmappable hosts fail closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop lower/UPPER pwd as bare keys (connection-string Pwd stays) and reject path-shaped and Go test-name values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jobSchedule takes the zone as a parameter instead of reloading config per job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore bare pwd/PWD keys; secrets that start with / or Test are redacted again under every other key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Directory rules apply only at or below the workspace root; .docker, *.key and .env.*.example are narrowed; .envrc, *.env, shell history and keyrings added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Variable and env references, member chains, paths, ARNs, timestamps and
lowercase resource names are no longer redacted as bare-key credentials.

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

IsSensitiveReadPath now uses the same home-sensitive prefixes as ClassifyPath so the two cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Standalone, appended, exported and env -S assignment forms now classify their value as a command.

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

Escapes like \w, \S, / and a space, and user selectors without a name pattern, now escalate; a literal \. no longer does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documents such as capital-plan.key or presentation-client.key load; server.key, tls.key, client.key and key directories stay protected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Polling tabs on a deleted or tokenless session no longer spend the shared session-lookup budget.

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

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

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

Broad parents (.config, .docker, .cargo, .gem, .terraform.d) are narrowed to their credential children; ClassifyPath keeps the whole shared prefixes.

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

Values stored into PROMPT_COMMAND/PS*/BASH_FUNC_* by printf -v or a here-string read are classified as commands; other input sources are unknown.

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

Accept one 20+ char letters+digits run (lowercase or UPPER) incl. quoted JSON/YAML. A value with '/' is a path when any segment is word-only (len>=3, no digit or base64 chars); slash-prefixed base64 like /aB3d... stays redacted. UUIDs and separator-joined groups stay visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 12, 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 e64b679 Commit Preview URL

Branch Preview URL
Oct 12 2026, 12:59 PM

jkyberneees and others added 2 commits October 12, 2026 14:41
… check

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

The git default test compared against the default policy, which depends on
whether the checkout running it is an armed repository; it now compares
against git push outside any repo and checks a git push denylist entry. The
linearity test checks 4x scaling and keeps its absolute bound off -race/-short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit 3df53de into main Oct 12, 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.

1 participant