Skip to content

Security review findings - #8

Open
alvagante wants to merge 2 commits into
mainfrom
fix/security-review-findings
Open

Security review findings#8
alvagante wants to merge 2 commits into
mainfrom
fix/security-review-findings

Conversation

@alvagante

Copy link
Copy Markdown
Member

No description provided.

alvagante and others added 2 commits September 2, 2026 11:20
A security review of the tree found three places where the code did not
do what its own comment claimed, plus two smaller ones. All five are
fixed here with regression tests; the first two are confirmed
exploitable and were verified against live net/url, net/http and git
behaviour rather than reasoned about.

A File `source` value reached the compiler's file_content endpoint as an
unescaped path concatenation. It is the only one in the tree: every other
URL this codebase builds escapes its interpolation or is a constant. A
`source` is a parameter of a File resource in the candidate catalog, and
the candidate catalog is compiled from the change under review, so
`puppet:///../../pdb/query/v4/catalogs/<node>` was attacker-shaped input
steering an mTLS-authenticated GET. net/url does not remove dot segments
from a path it is handed and net/http sends the request line as written,
so the traversal survived intact to the wire. Bounded by the transport,
which pins scheme and authority and refuses a cross-authority redirect,
and by this package never returning retrieved bytes, so it reached other
paths on a host PIACE is already authorized against and yielded a digest
rather than content. parsePuppetSourceURI now returns an error and
refuses an empty, "." or ".." segment and a NUL byte.

The inference client had no redirect policy. internal/transport, which
carries no credential, has an explicit and tested one; internal/inference,
which carries the only bearer token in PIACE, had none, and that
asymmetry was the defect. net/http keeps an Authorization header across a
redirect whenever the target host is the original host or a subdomain of
it, judged on the host alone, so an endpoint answering `302 Location:
http://<same host>` got the token in cleartext. checkRedirect now refuses
any redirect that leaves https or changes authority. HTTPClient becomes
unexported behind SetHTTPClient at the same time: an exported field that
five tests and cmd/piace assigned to was a policy that could be dropped
by assignment, which is the wrong property for the one client holding a
credential.

The text report passed control characters through. It is the format that
goes to a terminal, and almost everything it interpolates is untrusted: a
resource title and a parameter value come from the candidate catalog, a
change assessment's summary is prose a model wrote. `ESC[2J ESC[1;1H`
followed by a forged outcome line is enough to make a run that exits 30
read as clean to the person deciding whether to merge it. One textf
helper now escapes C0, DEL and C1 in every line, preserving only the
newlines the format string itself contributes. The JSON and HTML reports
are deliberately unchanged: JSON escaping already makes a control
character inert and that document's canonical checksum is what explain
ties an assessment to, and html/template covers the HTML.

Refs reaching git are validated. On a fork pull request the base and head
refs are branch names whoever opened the change chose, and a ref of
`--output=<path>` reaching `git diff` positionally is an arbitrary file
write on the runner holding the catalog-reader identity. As it happens
`merge-base` runs first and rejects an unknown option, so that value
dead-ends before `diff`, but a guard that depends on the argument order
of the first of four commands is not a guard. validateRef refuses a
leading "-" before any git command runs, and --end-of-options covers what
a future reordering might otherwise let through.

Two smaller ones. The inference client read its response body through a
LimitReader without the extra byte internal/transport uses to tell a
body that reached the limit from one that exceeded it, so an oversized
response was reported as "not a chat completion" and sent an operator
looking at the wrong thing. And --debug-dump-dir wrote through
os.WriteFile, which follows a symlink already sitting at the path and
leaves that file's existing mode, applying 0600 only to a file it creates
itself; those bodies are the unredacted ones and a dump directory is
often /tmp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Anthropic's OpenAI-compatible endpoint documents itself as ignoring
  `response_format` rather than rejecting it, so a request that relied on
  structured output alone asked for nothing. TaskPrompt now describes the
  exact JSON shape unconditionally, and a test holds it to ResponseSchema
  so the two cannot drift.
- Interpret unwraps a Markdown code fence that wraps the whole response,
  recording a warning, rather than discarding a complete assessment over
  its packaging. Only a fence that both opens and closes the trimmed
  reply is removed; backticks inside a rationale are left alone.
- Label the impact-estimate section "Potential impact estimate" with a
  capital P across every output format and its golden files.
- Color a successful differences_allowed run green: it shares exit 0 with
  clean, and yellow is reserved for the advisory medium-risk indication,
  not for a run that succeeded.
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