Skip to content

feat(broker): restrict services to specific HTTP methods - #324

Open
cjohnhanson wants to merge 1 commit into
Infisical:mainfrom
cjohnhanson:feat/service-methods
Open

feat(broker): restrict services to specific HTTP methods#324
cjohnhanson wants to merge 1 commit into
Infisical:mainfrom
cjohnhanson:feat/service-methods

Conversation

@cjohnhanson

Copy link
Copy Markdown

Implements #323: an optional methods field on a service so the credential is attached only to requests using the listed HTTP methods. methods: [GET, HEAD] brokers a privileged token read-only.

What's in here

  • broker.Service gains Methods []string. Omitted = all methods (backward compatible). Validation rejects an explicit methods: [], uppercases values on ingest, and checks them against GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS.
  • The check is a post-selection gate on the single MatchService winner, parallel to the existing IsEnabled() check — a method-excluded winner is denied outright with no fall-through to a broader sibling. It runs at the Inject boundary before either credential path resolves, so a denied request gets the credential in no form: no auth headers, no substitutions (including on passthrough services).
  • Denial is 403 with error code method_not_allowed (consistent with every existing broker-policy denial on the ingress) and a message listing the allowed methods.
  • Method-restricted services also have the X-HTTP-Method-Override / X-Method-Override / X-HTTP-Method headers and the _method query parameter stripped before forwarding, so an allowed method can't stand in for a denied one. Query stripping preserves the surviving pairs byte-for-byte (no re-encoding), so signed URLs keep their signatures. The _method form in a POST body is not inspected.
  • Docs: docs/learn/services.mdx gains a "Restricting HTTP methods" section; the matching-rules note now points at it.

Tests

  • broker: normalization, unsupported/duplicate/empty-list rejection, AllowsMethod case-sensitivity fail-closed.
  • brokercore: allowed method injects; denied method returns ErrMethodNotAllowed with zero credential-store touches — including the passthrough-with-substitutions case.
  • mitm: unit table for _method query stripping, plus an end-to-end proxy test asserting the upstream sees the override header and _method param stripped on a restricted service and untouched on an unrestricted one.

Full suite passes (go test ./...).

Not in this PR

Web UI service form, agent proposal flow, and SDK typings don't know about methods yet — the field is additive, so they keep working; happy to follow up on those (and on whether override-stripping should be split out, per the issue) once the shape is agreed.

Adds an optional methods list to a service. When present, the credential
is attached only to requests using a listed method; any other method is
denied with 403 method_not_allowed before either credential path (auth
headers or substitutions) resolves. The check runs on the single
MatchService winner after selection, parallel to IsEnabled — no
fall-through to a broader sibling. Method-restricted services also have
the X-HTTP-Method-Override / X-Method-Override / X-HTTP-Method headers
and the _method query parameter stripped before forwarding.

Omitted methods = all methods; an explicit empty list is rejected.
Values are uppercase-normalized on ingest and compared case-sensitively
at request time.
@infisical-cla-app

infisical-cla-app Bot commented Jul 18, 2026

Copy link
Copy Markdown

CLA satisfied. All contributors have signed the current CLA. The cla/signed check is passing.

@cjohnhanson
cjohnhanson marked this pull request as ready for review July 18, 2026 23:54
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an optional methods list to broker services so the injected credential is attached only when the request's HTTP method matches. The check runs after service selection, parallel to IsEnabled, with no fall-through to a sibling on denial. Method-restricted services also have the well-known override surfaces (X-HTTP-Method-Override, X-Method-Override, X-HTTP-Method headers and _method query param) stripped before forwarding.

  • broker.Service gains Methods []string; NormalizeAndValidateMethods uppercases, deduplicates, and rejects unsupported or explicitly-empty lists; AllowsMethod does a case-sensitive comparison against the normalized list so non-uppercase input fails closed.
  • CredentialProvider.Inject gains a method parameter; MethodNotAllowedError wraps ErrMethodNotAllowed and carries the denied method and the allowed list for a structured 403 response.
  • forwardRequest strips _method from outURL.RawQuery (before outReq is built) and passes the override header names to ApplyInjection so they are not forwarded; the OAuth 401-retry path receives r.Method too, preserving the method check on refresh.

Confidence Score: 4/5

Safe to merge; the new method gate is well-structured with correct ordering (strip before outReq construction, check before credential access), good test coverage, and backward-compatible defaults.

The core logic is correct end-to-end: the method check fires before any credential path, outURL.RawQuery is stripped before the outbound request is built, and headers are stripped via ApplyInjection. The two findings are non-blocking quality observations — percent-encoded key bypass is a real but very unlikely upstream compatibility edge case, and the emit status coupling is a latent fragility rather than a current defect.

internal/mitm/forward.go — the stripMethodOverrideParam function and the emit status assignment for the method_not_allowed case are the two spots worth a second look.

Important Files Changed

Filename Overview
internal/broker/broker.go Adds Methods []string field, NormalizeAndValidateMethods, and AllowsMethod. Validation rejects empty lists and duplicates, uppercases on ingest, and mutates via cfg.Services[i] not a range copy. Logic is correct and well-tested.
internal/brokercore/credential.go Adds method parameter to Inject, inserts AllowsMethod gate after IsEnabled check (before credential resolution), and populates MethodRestricted in InjectResult. Method check correctly fires before any secret is accessed.
internal/mitm/forward.go Passes r.Method to Inject; strips method-override headers and _method query param for restricted services. outURL.RawQuery is stripped before outReq is created (correct order). Minor: percent-encoded _method keys are not stripped; emit status for method_not_allowed relies on coincidental 403 default.
internal/brokercore/brokercore.go Adds ErrMethodNotAllowed case to WriteInjectError with structured message extraction from *MethodNotAllowedError. Returns 403 consistent with other broker policy denials.
internal/brokercore/errors.go Adds ErrMethodNotAllowed sentinel error following the same pattern as ErrServiceDisabled.
internal/brokercore/credential_methods_test.go Tests allowed/denied method injection, confirms zero credential-store touches on denial, and covers passthrough+substitutions denial. Tests are thorough and well-structured.
internal/mitm/forward_methods_test.go Unit table for stripMethodOverrideParam and an end-to-end proxy test confirming override header and _method param are stripped for restricted services and preserved for unrestricted ones.
docs/learn/services.mdx Adds Restricting HTTP methods section with YAML example, validation rules, and important caveats (no fall-through, override stripping, WebSocket note). Documentation is accurate and thorough.

Reviews (1): Last reviewed commit: "feat(broker): restrict services to speci..." | Re-trigger Greptile

Comment thread internal/mitm/forward.go
Comment on lines +453 to +469
func stripMethodOverrideParam(rawQuery string) string {
if !strings.Contains(rawQuery, "_method") {
return rawQuery
}
parts := strings.Split(rawQuery, "&")
kept := parts[:0]
for _, p := range parts {
key := p
if i := strings.IndexByte(p, '='); i >= 0 {
key = p[:i]
}
if key == "_method" {
continue
}
kept = append(kept, p)
}
return strings.Join(kept, "&")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 %-encoded _method key bypasses stripping

stripMethodOverrideParam compares the raw key bytes against the literal string "_method". A query string such as %5Fmethod=DELETE has key %5Fmethod, so the check key == "_method" is false and the parameter is forwarded. Upstreams that decode percent-encoded parameter names before honoring _method (older Rails/Rack, some PHP frameworks) would still see an effective method override on a method-restricted service, provided the outer HTTP method is in the allowed list.

Comment thread internal/mitm/forward.go
Comment on lines +258 to 263
case errors.Is(err, brokercore.ErrMethodNotAllowed):
errCode = "method_not_allowed"
}
brokercore.WriteInjectError(w, err, target, scope.VaultName, p.baseURL)
emit(status, errCode)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 emit carries stale status for ErrMethodNotAllowed

When ErrMethodNotAllowed is matched, status stays at its default http.StatusForbidden (403). That coincidentally matches what WriteInjectError sends. But if WriteInjectError ever changes its response code for this error (e.g. to 405), emit would report the wrong code to the event/metrics sink. Setting status = http.StatusForbidden explicitly in the case, like the credential-missing case does for 502, removes the silent dependency between the two call sites.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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