Skip to content

fix(proxy): refuse method-override headers; add GCP observability curl example - #15

Merged
paveq merged 2 commits into
mainfrom
docs/proxy-curl-example
Sep 23, 2026
Merged

paveq merged 2 commits into
mainfrom
docs/proxy-curl-example

Conversation

@paveq

@paveq paveq commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Why

Proxy tools (#8) have no example under examples/. While checking the route rules for one, I found that Google APIs route by X-HTTP-Method-Override. That header let a request get past the proxy's method rules. This PR fixes that bypass and adds the example.

1. Fix: refuse method-override headers (src/proxy/server.rs)

Route rules match the method on the request line. Google front ends use X-HTTP-Method-Override instead when it is present, on GET and POST. Checked against logging.googleapis.com:

Request Status Meaning
GET /v2/entries:list 404 no GET method on this path
GET /v2/entries:list + X-HTTP-Method-Override: POST 403 routed to the POST method, auth required
POST /v2/projects/x/logs/y 404 no POST method on this path
POST /v2/projects/x/logs/y + X-HTTP-Method-Override: DELETE 403 routed to logs.delete

So the README's own example, deny = ["DELETE /**"], did not stop POST + X-HTTP-Method-Override: DELETE. A GET /…/** allow rule also let writes through.

vet_request now returns 403 when X-HTTP-Method-Override, X-HTTP-Method or X-Method-Override is present, whatever its value. The proxy refuses the request instead of stripping the header. Stripping would forward a different request than the tool sent, while a clear 403 tells the agent to use -X.

Not covered: frameworks (Laravel, Symfony, Rails) that read _method from a POST's form body or query string. The proxy doesn't parse bodies. SECURITY.md lists this under residual risks.

Docs: the SECURITY.md refusal table and residual risks, a SKILL.md rule for agents, the README rules section, and the flow diagram in docs/proxy-tools-design.md.

2. Example: examples/gcp-observability-curl.toml

This file sets up curl as a proxy tool for Cloud Trace, Cloud Monitoring (v3, MQL, PromQL, dashboards) and Cloud Logging. gcloud covers these REST APIs only partly. The README's proxy-tools section links to it.

  • Every path is pinned to my-project, and only read verbs are allowed. The one exception is POST /v2/entries:list, which has no project in its path.
  • deny is a carve-out. The monitoring route denies notificationChannels/** and uptimeCheckConfigs/** inside the broad GET /v3/projects/my-project/**. Channel labels and uptime-check headers can carry third-party credentials. Redaction only knows the declared secrets.
  • IAM is the stated boundary. The header comment says why: entries:list names its projects in the body, and redaction doesn't cover undeclared credentials.

Verification

  • cargo fmt --check, cargo clippy --all-targets -D warnings: clean.
  • cargo test --lib: 480 passed. This includes the new method_override_headers_are_refused, which covers every header in any letter case, on GET and POST, with an allow-everything route.
  • cargo test --test proxy_e2e_integration: 6 passed.
  • A throwaway test loaded every examples/*.toml. All load.
  • A throwaway test checked 20 request cases against the example's find_route / permits: allowed reads, refused writes, the deny carve-outs, another project, and an unrouted host. All matched the intended decision.

🤖 Generated with Claude Code

Proxy tools landed in #8 without an example under examples/. This adds
one for Cloud Trace, Monitoring (MQL, PromQL, dashboards) and Logging,
the REST APIs gcloud doesn't fully cover.

The routes pin every path to one project and allow read verbs only.
The monitoring route also denies notificationChannels and
uptimeCheckConfigs: those objects can hold third-party credentials,
and the redactor only knows the declared secrets.

The header comment says the SA's IAM roles are the real boundary. Two
reasons:
- Google front ends honor X-HTTP-Method-Override on GET and POST, so a
  method rule doesn't stop a write on that path. Verified against
  logging.googleapis.com: GET entries:list with the override set to
  POST returns 403 (routed) instead of 404.
- entries:list names its projects in the body, which the proxy doesn't
  inspect.
Route rules match the method on the request line, but Google front ends
route by X-HTTP-Method-Override when it is present, on GET as well as
POST. So `deny = ["DELETE /**"]` did not stop
`POST /x` + `X-HTTP-Method-Override: DELETE`, and a `GET /v3/**` allow
rule admitted writes. Verified against logging.googleapis.com: GET
/v2/entries:list returns 404 without the header and 403 (routed to the
POST method, auth required) with `X-HTTP-Method-Override: POST`.

vet_request now answers 403 when X-HTTP-Method-Override, X-HTTP-Method
or X-Method-Override is present, whatever its value. The request is
refused, not stripped: stripping would forward a different request than
the tool sent, and a clear 403 tells the agent to use -X.

Frameworks that take `_method` from a POST's form body or query string
(Laravel, Symfony, Rails) are out of reach, because the proxy doesn't
parse bodies. SECURITY.md lists that as a residual risk.
@paveq paveq changed the title docs(examples): curl proxy-tool example for GCP observability APIs fix(proxy): refuse method-override headers; add GCP observability curl example Sep 23, 2026
@paveq
paveq merged commit 480b633 into main Sep 23, 2026
4 checks passed
@paveq
paveq deleted the docs/proxy-curl-example branch September 23, 2026 11:52
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