Skip to content

fix(targeting): sanitize context-agent validation error responses - #484

Merged
bokelley merged 2 commits into
mainfrom
maintainer/pr-474-sanitize-errors
Sep 4, 2026
Merged

fix(targeting): sanitize context-agent validation error responses#484
bokelley merged 2 commits into
mainfrom
maintainer/pr-474-sanitize-errors

Conversation

@bokelley

@bokelley bokelley commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Policy-compliant relay of external contribution #474 so repository-default CodeQL can run. The original contributor remains the commit author; maintainer and Ladon reviews plus CI passed on #474.

Problem

targeting/contextagent/handler.go's ServeHTTP echoes the raw
err.Error() from tmproto.ValidateContextRequest directly into the HTTP
error response body:

if err := tmproto.ValidateContextRequest(&req); err != nil {
    writeError(w, tmproto.SafeRequestIDForEcho(req.RequestID), tmproto.ErrorCodeInvalidRequest, err.Error(), http.StatusBadRequest)
    return
}

A caller sending a malformed context_match_request gets back
validator-internal text such as "property_id contains invalid characters" or "seller_agent_url exceeds maximum length of 2048"
instead of a generic message, and the failure is never logged
server-side (h.logger is otherwise used elsewhere in this file, just
not here). This is a direct violation of AGENTS.md's error-message
invariant:

Error messages must be generic. Internal errors return "internal error" to callers. Details go to structured logs (slog) inside the
service boundary. Never echo err.Error() in HTTP responses.

Just as importantly, this exact pattern was already identified and
swept out of the rest of the codebase: #190 ("Sweep TMP validator
errors for user-controlled value echoes") and #201 ("Audit TMP
validation rejection logging after error sanitization") were fixed by
PR #210, which changed router.HandleContextMatch,
router.HandleIdentityMatch, and identityagent's ServeHTTP to log
the validation failure server-side via a logValidationFailure helper
and return a generic "invalid request" to the caller. That PR's diff
does not touch targeting/contextagent/handler.go — the handler
actually wired into the cmd/context-agent binary — so it silently
kept the pre-#210 behavior. This PR closes that gap.

Verification

Fix

Add a logValidationFailure helper to
targeting/contextagent/handler.go, mirroring
identityagent.identityHandler.logValidationFailure field-for-field
(method, path, error, and either request_id or
request_id_valid=false depending on tmproto.SafeRequestIDForEcho),
logged via h.logger.Warn. The HTTP response now always gets the
generic "invalid request" message on a validation failure, matching
identityagent's and router.go's behavior for the same check.

Test proof

Added targeting/contextagent/handler_test.go with two regression
tests:

  • TestContextHandlerValidationErrorIsGenericAndLogged — sends a
    request with an invalid property_id, asserts the HTTP response
    message is exactly "invalid request" and never contains
    "property_id", and asserts the structured log does contain the
    detailed validator message ("property_id contains invalid characters") plus method/path/request_id.
  • TestContextHandlerInvalidRequestIDIsNotEchoed — sends a request
    whose request_id itself fails validateSafeID ("bad/id"),
    asserts it's neither echoed in the response nor written verbatim to
    the log (elided in favor of request_id_valid=false).

Confirmed both are real regression tests: reverted the handler.go
source change (git stash on that file only, keeping the new test
file), re-ran go test ./targeting/contextagent/... -run TestContextHandler -v and both tests failed against the pre-fix
code with exactly the expected diffs (response message
"property_id contains invalid characters" / "request_id contains invalid characters" instead of "invalid request", and empty logs).
Restored the fix and re-ran — both pass.

go build ./...                                   # root workspace, ok
go vet ./...                                      # root workspace, ok
cd targeting && go build ./... && go vet ./... && go test ./...   # ok, all packages
cd cmd/router && go test ./...                     # ok
cd reference/context-agent && go test ./...         # ok
cd e2e && go test ./... -skip TestPerformance_EndToEnd   # ok (perf/throughput
                                                     # subtest fails locally on
                                                     # unmodified main too, due
                                                     # to macOS ephemeral-port
                                                     # exhaustion under load —
                                                     # unrelated to this change)

No schema changes, no new dependencies, no behavior change on the
happy path — only the shape of the 400 response and server-side
logging on the validation-failure path for POST /context.

sujanchalla0510 and others added 2 commits August 30, 2026 12:01
The context-agent HTTP handler (targeting/contextagent/handler.go)
echoed tmproto.ValidateContextRequest's raw err.Error() text straight
into the HTTP error response body, e.g. "property_id contains invalid
characters" or "seller_agent_url exceeds maximum length of 2048". This
violates AGENTS.md's generic-error-message invariant ("Never echo
err.Error() in HTTP responses ... details go to structured logs
(slog) inside the service boundary") and, unlike every other place
that runs the same ValidateContextRequest/ValidateIdentityRequest
check, never logged the failure server-side at all — so an operator
had zero record of why a request was rejected.

This exact pattern was swept and fixed elsewhere in this repo by
#190/#201 (landed in PR #210): router.HandleContextMatch,
router.HandleIdentityMatch, and identityagent's ServeHTTP were all
changed to log the validation error server-side via a
logValidationFailure helper and return a generic "invalid request"
message to the caller. That sweep did not touch
targeting/contextagent/handler.go, which is the handler actually
wired into the cmd/context-agent binary, so it kept the pre-#210
behavior.

Fix: add a logValidationFailure helper to the context-agent handler,
mirroring identityagent's implementation exactly (method, path, error,
and either request_id or request_id_valid=false, logged via
slog.Warn), and replace the err.Error()-in-response call with the
generic "invalid request" message.

Verification: confirmed via git blame that this line predates and was
untouched by the #210 sweep, and confirmed no open issue or PR already
covers this file. Regression tests added in handler_test.go assert
the HTTP response never contains the validator's field-specific text
while the structured log does; both tests were checked to fail against
the pre-fix source (reverting the handler.go change while keeping the
tests) and pass after restoring it.

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — clean, well-scoped security fix.

This PR replaces an err.Error() echo on the context-agent validation path with a generic "invalid request" response, adding a server-side logValidationFailure helper that mirrors identityagent field-for-field (method/path/error, request_id only when SafeRequestIDForEcho passes, else request_id_valid=false). Two verified regression tests confirm the response never leaks validator detail while the log retains it.

Checked:

  • No critical/high/medium findings from the reviewer.
  • high_risk is true only because files match targeting/** — one file (modified) with no medium-or-higher concern, one file (added) test. Per the high-risk flag rules, a modification with no medium finding is presumed safe and a new file is normal scaffolding; neither warrants escalation.
  • gated_paths is false, so the gated-paths hard gate does not apply despite review_decision: REVIEW_REQUIRED.
  • No no-auto-approve team match.
  • No prior decision.

Rows 1–8 do not fire (no findings at all). Falls through to row 9 → approve. One non-blocking follow-up was noted on the adjacent unsupported-adcp_major_version branch, which is pre-existing and outside this diff.

@bokelley
bokelley merged commit 8d7d4b4 into main Sep 4, 2026
30 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