Skip to content

Fix P08, P10, P11, and P12 from the 2026-09-29 medium-findings audit - #11

Merged
MuffinUser merged 5 commits into
mainfrom
worktree-audit-medium-findings
Sep 29, 2026
Merged

MuffinUser merged 5 commits into
mainfrom
worktree-audit-medium-findings

Conversation

@MuffinUser

@MuffinUser MuffinUser commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • P08: Clean/cleanValues mistook a plaintext value/field starting with ENC[ for already-encrypted content (bare prefix check) and left it committed as plaintext. Replaced with tag.LooksEncrypted, which requires the full tag shape (and still tolerates the pre-rotation legacy tag format, unlike a full tag.Parse, so the existing legacy-migration test keeps passing).
  • P12: nebel init (reportCheckoutResult) always exited 0 even when a managed file's checkout genuinely failed (AC-6.9 tamper/corruption). Now returns ErrCheckoutFailed in that case, while the intentional, documented "still needs a key version" passthrough (AC-6.11) keeps exiting 0 as before.
  • P10: yamlHandler.Render only escaped backslash and the closing quote. A decrypted string with a literal control character (e.g. a real newline byte) got spliced into a double-quoted scalar as-is and silently reinterpreted by YAML's line-folding rules on the next parse. Fixed with proper escaping — but the natural \xHH hex-escape fallback turned out to be unusable: found and confirmed a real bug in the goccy/go-yaml lexer that mistracks a scalar token's source span for any \x/\u/\U escape. Render now returns (string, error); YAML's own single-character escapes cover 9 control codes, everything else is refused outright (ErrControlCharacterUnsupported) rather than risk that lexer bug corrupting a later Locate.
  • P11: Locate/Leaves only ever looked at the first ----separated YAML document, silently ignoring the rest. Now rejected outright with ErrMultiDocumentUnsupported (per discussion: reject rather than attempt cross-document field resolution, which raises its own ambiguity).
  • Companion validation/mitigation-plan doc (audits/2026-09-29-medium-findings-validation-and-mitigation-plan.md) covering all five of the audit's medium-priority findings (P08–P12); P09 (rotation atomicity / password loss on failure) remains open and is intentionally not included here.
  • New spec ACs: 5.14 (control-character escaping) and 5.15 (multi-document rejection) in specs/05-format-handlers.md.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (full suite, unchanged elsewhere)
  • New regression tests, each confirmed to fail against the pre-fix code: TestCleanEncryptsPlaintextStartingWithEncPrefix, TestCleanLeavesLegacyWholeFileTagAlone, TestCleanValuesEncryptsFieldStartingWithEncPrefix, TestReportCheckoutResultFailsOnGenuineCheckoutFailure, TestReportCheckoutResultSucceedsWhenOnlyMissingKeyVersion, TestRenderEscapesYAMLControlCharacters, TestRenderRejectsUnescapableControlCharacters, TestLocateAndLeavesRejectMultiDocumentYAML, TestLocateAndLeavesAcceptSingleDocumentWithLeadingMarker, TestValueRoundTripPreservesEmbeddedNewline, TestSmudgeValuesRejectsUnrenderableControlCharacter

🤖 Generated with Claude Code

MuffinUser and others added 5 commits September 29, 2026 16:06
Clean's whole-file path and cleanValues both used tag.IsEncrypted (a
bare "ENC[" prefix check) to decide a value was already encrypted and
needed no further processing. A plaintext value or field that merely
starts with "ENC[" without being a well-formed tag satisfied that check
and was committed as plaintext instead of being encrypted.

The new tag.LooksEncrypted requires the full ENC[...] delimiter shape
and a recognized algorithm name, but deliberately still tolerates a
pre-rotation legacy tag missing its "key:" field (a full tag.Parse does
not: an earlier attempt using Parse as the gate regressed the existing
legacy-tag migration test, since a real legacy tag genuinely fails
Parse on the missing key field alone). tag.IsEncrypted itself is
unchanged and still correct as smudge's pre-Parse fast path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
reportCheckoutResult printed both categories of problem it can find —
a genuine per-file checkout failure (AC-6.9 tamper/corruption) and a
file merely waiting on a key version this clone hasn't fetched yet
(AC-6.11's intentional, recoverable passthrough) — but always returned
nil either way. A script relying on the exit code alone could not tell
`nebel init` left unusable content behind.

The new ErrCheckoutFailed is returned only when a genuine checkout
failure is present. The AC-6.11 case deliberately keeps exiting 0:
TestInitVersionRecoversContentFromBeforeRotation already asserts that
case succeeds, since it's a documented, expected, recoverable state
(`nebel init --version N` fixes it), not a failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verifies P08-P12 from the 2026-09-29 repository analysis (the five
findings the existing high-findings plan explicitly left out of
scope), records that P08 and P12 are now fixed, and lays out what's
left for P09-P11. Resolves the open design question on P11 (reject
multi-document YAML rather than attempt cross-document field
resolution).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ejection

P10: yamlHandler.Render escaped only backslash and the closing quote.
A decrypted string containing a literal control character (a real
"\n" byte, not the two-character escape) was spliced into a
double-quoted scalar as-is, then silently reinterpreted by YAML's own
line-folding rules on the next parse with no error ever raised.

The natural fallback for characters with no YAML single-character
escape - a "\xHH" hex escape - turned out to be unusable: the
goccy/go-yaml parser this codebase depends on has a confirmed bug
mistracking a scalar token's source span for any \x, \u, or \U escape
(reproduced independent of the escaped value), which would corrupt the
next Locate of that same field instead of failing where the problem
actually is. Render's signature now returns (string, error); YAML's
own single-character escapes (\0 \a \b \t \n \v \f \r \e) are used
where they exist, and anything else refuses outright with a new
ErrControlCharacterUnsupported rather than risk the broken hex-escape
path - the same choice already made for block scalars.

P11: Locate and Leaves both operated on file.Docs[0] only, silently
ignoring every "---"-separated document after the first. A configured
path matching a leaf only in a later document was never located,
never encrypted, and never even reported as missing. Both now return
a new ErrMultiDocumentUnsupported when more than one document is
present, rather than resolve an ambiguity (the same path present in
two documents - which one does a single Encrypt entry protect?) this
codebase does not attempt to solve.

Bundled together per the existing mitigation plan's own note: both
touch the same YAML rendering/parsing code path and test harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the fix, the unplanned goccy/go-yaml hex-escape lexer bug
found and worked around while fixing P10, and the tests added for
both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MuffinUser MuffinUser changed the title Fix P08 and P12 from the 2026-09-29 medium-findings audit Fix P08, P10, P11, and P12 from the 2026-09-29 medium-findings audit Sep 29, 2026
@MuffinUser
MuffinUser marked this pull request as ready for review September 29, 2026 14:24
@MuffinUser
MuffinUser merged commit 8599e77 into main Sep 29, 2026
5 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