Skip to content

[Bug]: Gateway Controller does not validate policy params for LLM operationPolicies and the deprecated policies list #3381

Description

@thivindu

Please select the area the issue is related to

Gateway

Please select the aspect the issue is related to

Aspect/API (API backends, definitions, contracts, interfaces, OpenAPI)

Description

PolicyValidator validates policy params against the policy definition's declared JSON Schema for most artifact types, but silently skips it for two of the five policy collections an LlmProvider/LlmProxy can declare.

validateLLMPolicyRefs resolves the policy reference for the operation-level and deprecated lists and then discards the resolved definition, so the params are never schema-checked:

for i, policy := range *operationPolicies {
    _, errs := pv.validatePolicyRef(policy.Name, policy.Version, fmt.Sprintf("spec.operationPolicies[%d]", i))
    errors = append(errors, errs...)
}

Only the global (api-level) list routes through validatePolicy, which performs the schema check.

Coverage today

Artifact kind Policy collection Name + version Params vs. schema
RestApi spec.policies
RestApi spec.operations[].policies
Mcp spec.policies
LlmProvider / LlmProxy spec.globalPolicies
LlmProvider / LlmProxy spec.operationPolicies[].paths[].params not validated
LlmProvider / LlmProxy spec.policies[].paths[].params (deprecated) not validated

Entry points for the rows that do work: ValidateRestAPIPolicies and ValidateMCPProxyPolicies.

Impact

A misconfigured operation-level LLM policy deploys successfully instead of being rejected at deploy time. Missing required params, out-of-range values, and unknown properties all pass. The failure surfaces later at runtime, where the policy is dropped or misbehaves with no deploy-time signal — and the same params on the same policy are correctly rejected when attached as a globalPolicy, which makes the behavior look arbitrary to users.

Nothing downstream closes the gap:

  • llm_deployment.go validates the source config after Transform, so the derived RestAPI (which does carry these params in operations[].policies) is never passed to ValidateRestAPIPolicies — see the provider and proxy paths.
  • The event-listener path only calls CoerceRestAPIPolicies — coercion, no validation: provider, proxy.

Note that coercion already walks all three LLM collections correctly — coerceLLMPolicyRefs iterates paths[j].Params. It is specifically the validation half that is asymmetric.

Root cause

Introduced in 18ab436e0 ("Validate policy name/version existence for LLM providers and proxies"), whose scope was name/version only. The inline comment shows the assumption behind it:

// Global (api-level) policies carry params, so reuse validatePolicy to also validate them.

Operation-level and deprecated policies do carry params - nested one level deeper, under paths[].params.

Notes for the fix

Validate the source config's per-path params, not the derived RestAPI.

The LLM→RestAPI transform merges the provider template's extraction params — requestModel, responseModel, promptTokens, completionTokens, totalTokens, remainingTokens — into every operation-level policy attachment via mergeParams, at three call sites. None of the shipped policy definitions declare those keys, and most set additionalProperties: false — so schema-validating the post-transform result rejects essentially every LLM operation-level policy with Additional property requestModel is not allowed. The user-authored params validate clean on their own.

Two details the fix must get right:

  • A nil/absent params map still has to be validated, or omitting params: entirely bypasses the schema's required list.
  • Coerce before validating, since template rendering always yields strings ({{ env "LIMIT" }}"100" for an integer param) — matching how validatePolicy handles api-level params.

Steps to Reproduce

  1. Start the gateway (controller on :9090) and deploy an LlmProvider named openai-dp-1 — e.g. gateway/examples/llm-provider.yaml.

  2. Save an LlmProxy that attaches llm-cost-based-ratelimit as an operation-level policy with params that violate its schema — budgetLimits is required and the definition sets additionalProperties: false:

    apiVersion: gateway.api-platform.wso2.com/v1
    kind: LlmProxy
    metadata:
      name: param-validation-repro
    spec:
      displayName: Param Validation Repro
      version: v1.0
      context: /repro
      provider:
        id: openai-dp-1
      operationPolicies:
        - name: llm-cost-based-ratelimit
          version: v1
          paths:
            - path: /chat/completions
              methods: [POST]
              params:
                bogus: x          # unknown property; required budgetLimits missing
  3. Deploy it:

    curl --location 'http://localhost:9090/api/management/v1/llm-proxies' \
      --header 'Content-Type: text/yaml' \
      --user "$ADMIN_USERNAME:$ADMIN_PASSWORD" \
      --data-binary '@param-validation-repro.yaml'
  4. Contrast: move the identical policy block from operationPolicies to globalPolicies (params inline on the policy, no paths) and deploy again.

Expected

Both deploys are rejected with a policy validation error naming the offending param path.

Actual

  • globalPolicies → rejected, as expected:
    spec.globalPolicies[0].params: budgetLimits is required
    spec.globalPolicies[0].params: Additional property bogus is not allowed
    
  • operationPoliciesaccepted and deployed. No errors.

The deprecated spec.policies[].paths[].params list behaves the same as operationPolicies.

Severity Level of the Issue

Severity/Minor (Non-critical functionality. Can be fixed in future releases)

Environment Details (with versions)

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Area/AIGatewayAI Gateway runtime/control plane related issuesAspect/APIAPI definitions, contracts, OpenAPI, interfacesSeverity/MinorType/Bug

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions