fix: reject ambiguous inline key-auth config that bypasses duplicate check#434
fix: reject ambiguous inline key-auth config that bypasses duplicate check#434shreemaan-abhishek wants to merge 2 commits into
Conversation
…check
The Consumer webhook extracts the key-auth "key" from inline credential
config with Go's encoding/json struct decoder. On {"key":123,"key":"K"}
that decoder returns an error, so the webhook silently skips the duplicate
check, while downstream cjson (last-wins, no type abort) resolves the same
bytes to a live key "K". The only hard-deny credential check was evadable
by any Consumer creator who knows a colliding key.
Parse inline config aligned with cjson (exact-case, string-valued,
last-wins) and reject the ambiguous shapes that cause the divergence:
duplicate "key" members and a non-string "key" are hard admission
errors. Genuinely malformed JSON that cjson also rejects stays lenient.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe consumer webhook now uses streaming JSON parsing for inline ChangesInline key-auth credential validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
conformance test report - apisix modeapiVersion: gateway.networking.k8s.io/v1
date: "2026-07-17T11:02:08Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.3.0
implementation:
contact: null
organization: APISIX
project: apisix-ingress-controller
url: https://github.com/apache/apisix-ingress-controller.git
version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
failedTests:
- HTTPRouteInvalidBackendRefUnknownKind
result: failure
skippedTests:
- HTTPRouteHTTPSListener
statistics:
Failed: 1
Passed: 31
Skipped: 1
extended:
result: partial
skippedTests:
- HTTPRouteRedirectPortAndScheme
statistics:
Failed: 0
Passed: 11
Skipped: 1
supportedFeatures:
- GatewayAddressEmpty
- GatewayPort8080
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
- HTTPRoutePathRewrite
- HTTPRoutePortRedirect
- HTTPRouteQueryParamMatching
- HTTPRouteRequestMirror
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayHTTPListenerIsolation
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteBackendTimeout
- HTTPRouteParentRefPort
- HTTPRoutePathRedirect
- HTTPRouteRequestMultipleMirrors
- HTTPRouteRequestPercentageMirror
- HTTPRouteRequestTimeout
name: GATEWAY-HTTP
summary: Core tests failed with 1 test failures. Extended tests partially succeeded
with 1 test skips.
- core:
result: partial
skippedTests:
- TLSRouteSimpleSameNamespace
statistics:
Failed: 0
Passed: 10
Skipped: 1
name: GATEWAY-TLS
summary: Core tests partially succeeded with 1 test skips.
- core:
result: success
statistics:
Failed: 0
Passed: 12
Skipped: 0
name: GATEWAY-GRPC
summary: Core tests succeeded. |
conformance test report - apisix-standalone modeapiVersion: gateway.networking.k8s.io/v1
date: "2026-07-17T11:01:48Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.3.0
implementation:
contact: null
organization: APISIX
project: apisix-ingress-controller
url: https://github.com/apache/apisix-ingress-controller.git
version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
result: success
statistics:
Failed: 0
Passed: 12
Skipped: 0
name: GATEWAY-GRPC
summary: Core tests succeeded.
- core:
result: partial
skippedTests:
- HTTPRouteHTTPSListener
statistics:
Failed: 0
Passed: 32
Skipped: 1
extended:
result: partial
skippedTests:
- HTTPRouteRedirectPortAndScheme
statistics:
Failed: 0
Passed: 11
Skipped: 1
supportedFeatures:
- GatewayAddressEmpty
- GatewayPort8080
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
- HTTPRoutePathRewrite
- HTTPRoutePortRedirect
- HTTPRouteQueryParamMatching
- HTTPRouteRequestMirror
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayHTTPListenerIsolation
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteBackendTimeout
- HTTPRouteParentRefPort
- HTTPRoutePathRedirect
- HTTPRouteRequestMultipleMirrors
- HTTPRouteRequestPercentageMirror
- HTTPRouteRequestTimeout
name: GATEWAY-HTTP
summary: Core tests partially succeeded with 1 test skips. Extended tests partially
succeeded with 1 test skips.
- core:
result: partial
skippedTests:
- TLSRouteSimpleSameNamespace
statistics:
Failed: 0
Passed: 10
Skipped: 1
name: GATEWAY-TLS
summary: Core tests partially succeeded with 1 test skips. |
conformance test reportapiVersion: gateway.networking.k8s.io/v1
date: "2026-07-17T11:19:38Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.3.0
implementation:
contact: null
organization: APISIX
project: apisix-ingress-controller
url: https://github.com/apache/apisix-ingress-controller.git
version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
failedTests:
- GatewayModifyListeners
- TLSRouteSimpleSameNamespace
result: failure
statistics:
Failed: 2
Passed: 9
Skipped: 0
name: GATEWAY-TLS
summary: Core tests failed with 2 test failures.
- core:
failedTests:
- GatewayModifyListeners
result: failure
statistics:
Failed: 1
Passed: 11
Skipped: 0
name: GATEWAY-GRPC
summary: Core tests failed with 1 test failures.
- core:
failedTests:
- GatewayModifyListeners
result: failure
skippedTests:
- HTTPRouteHTTPSListener
statistics:
Failed: 1
Passed: 31
Skipped: 1
extended:
result: partial
skippedTests:
- HTTPRouteRedirectPortAndScheme
statistics:
Failed: 0
Passed: 11
Skipped: 1
supportedFeatures:
- GatewayAddressEmpty
- GatewayPort8080
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
- HTTPRoutePathRewrite
- HTTPRoutePortRedirect
- HTTPRouteQueryParamMatching
- HTTPRouteRequestMirror
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayHTTPListenerIsolation
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteBackendTimeout
- HTTPRouteParentRefPort
- HTTPRoutePathRedirect
- HTTPRouteRequestMultipleMirrors
- HTTPRouteRequestPercentageMirror
- HTTPRouteRequestTimeout
name: GATEWAY-HTTP
summary: Core tests failed with 1 test failures. Extended tests partially succeeded
with 1 test skips. |
…alue) Address review: parseInlineKeyAuthKey extracted a key from truncated or multi-top-level-value input instead of skipping it, contradicting the documented cjson-aligned leniency. Guard with json.Valid so only exactly one well-formed JSON value is parsed; duplicate keys stay valid and are still caught by the token walk. Add malformed-shape test cases.
| } | ||
|
|
||
| if keyCount > 1 { | ||
| return "", fmt.Errorf("key-auth credential config has duplicate \"key\" members") |
There was a problem hiding this comment.
Same comments as on the OSS half (apache/apisix-ingress-controller#2807) — the two diffs are identical, so both apply here.
I don't think this branch can fire in a real cluster. The apiserver collapses duplicate JSON object keys (last-wins) while decoding the request — before admission webhooks are called and before storage — so credential.Config.Raw never holds two key members.
I checked at the raw-byte level rather than trusting a round-trip through a JSON parser (which would collapse duplicates itself and hide the answer): registered a probe validating webhook on consumers, dumped the unparsed AdmissionReview body, and POSTed the PoC through the raw REST endpoint.
$ kubectl create --raw ".../consumers" -f - # {"key":123,"key":"victims-key"}
Warning: duplicate field "spec.credentials[0].config.key"
# raw bytes the webhook received:
{"credentials":[{"config":{"key":"victims-key"},"name":"c1","type":"key-auth"}],...
# occurrences of "key": -> 1 contains "key":123 -> False
The persisted object is that same normalized form, so the controller and cjson downstream see it too. So the old struct decoder parses the PoC fine and the duplicate check runs normally — the webhook and cjson never disagree.
Do you have a PoC that reaches the webhook with the duplicate intact? If it was built by calling ValidateCreate directly with hand-written Raw bytes, that would explain it — those bytes can't come out of the apiserver.
| Key string `json:"key"` | ||
| key, err := parseInlineKeyAuthKey(credential.Config.Raw) | ||
| if err != nil { | ||
| return "", fmt.Errorf("invalid key-auth credential config for Consumer %s/%s: %w", |
There was a problem hiding this comment.
This part I think is a real regression: it turns a per-credential skip into a hard admission failure, and the caller applies that to other objects too.
validateDuplicateKeyAuthCredentials loops over every existing Consumer sharing the gateway and does extractKeyAuthKeys(existing) -> return err. So one stored Consumer with e.g. {"key":123} now denies create/update of every unrelated Consumer on that gateway, with an error naming a different object.
That state is reachable, unlike the duplicate-key case: config is preserve-unknown-fields so the apiserver stores {"key":123} verbatim, and the webhook ships failurePolicy: Ignore, so anything created while the controller is down lands unvalidated.
// existing: {"key":123} newcomer: {"key":"my-own-unique-key"}, same gateway, no collision
validator := buildConsumerValidator(t, existing)
_, err := validator.ValidateCreate(context.Background(), newcomer)
require.NoError(t, err)Passes on master, fails here with invalid key-auth credential config for Consumer default/legacy: key-auth credential "key" must be a string.
Could the existing-consumers loop stay best-effort — skip a credential it can't read, as today — and hard-deny only on the incoming object? That keeps the stricter validation where it's actionable without letting one legacy object wedge a whole gateway.
What this PR does
The
Consumervalidating webhook extracts the key-authkeyfrom inline credential config with Go'sencoding/jsonstruct decoder. Given a duplicate-key object like{"key":123,"key":"K"}, that decoder returns a type-mismatch error, soextractCredentialKeylogs and skips the duplicate check entirely (returns"", nil). Downstreamcjson(last-wins, no type abort) resolves the same raw bytes to a live key"K".Net effect: the webhook's only hard-deny credential check is evadable by any Consumer creator who knows a colliding key. They submit an inline config the webhook can't read but ADC/cjson can, and a duplicate active key-auth credential lands, causing consumer-identity confusion downstream. Check-bypass only (attacker must already know the key), calibrated Low.
Fix
Parse inline key-auth config aligned with the downstream
cjsonsemantics (exact-case, string-valued, last-wins) via a token-level walk, and reject the shapes that cause the divergence instead of silently skipping:keymembers → hard admission error.key(number/bool/object/array) → hard admission error.cjsonwould also reject → still lenient ("", nil), so existing consumers with broken config are not suddenly denied.{"Key":"K"}is ignored (matches cjson, which only reads lowercasekey), removing a false-positive divergence in the other direction.Tests
TestConsumerValidator_DenyDuplicateKeyAuthCredential_ParserDivergencedrives the{"key":123,"key":"K"}PoC end-to-end throughValidateCreateand asserts admission is denied.TestParseInlineKeyAuthKeytable-tests the parser: valid key, duplicate members, number-then-string PoC, non-string, object value, null, missing key, exact-case, malformed, non-object.Fixes FINDING-015 (api7/rfcs#149). Syncs the same fix as apache/apisix-ingress-controller#2807.
Summary by CodeRabbit
keyfields in inline JSON are now detected and rejected more consistently.keyvalues no longer bypass validation.keyextraction and duplicate-key rejection behavior.