Repository navigation
fix: reject HTTPRoutePolicy with malformed vars instead of dropping them #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ee9458a
ad83406
43662e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -337,7 +337,7 @@ func (t *Translator) fillPluginFromHTTPRequestRedirectFilter(plugins adctypes.Pl | |
| plugin.URI = uri | ||
| } | ||
|
|
||
| func (t *Translator) fillHTTPRoutePoliciesForHTTPRoute(tctx *provider.TranslateContext, routes []*adctypes.Route, rule gatewayv1.HTTPRouteRule) { | ||
| func (t *Translator) fillHTTPRoutePoliciesForHTTPRoute(tctx *provider.TranslateContext, routes []*adctypes.Route, rule gatewayv1.HTTPRouteRule) error { | ||
| var policies []v1alpha1.HTTPRoutePolicy | ||
| for _, policy := range tctx.HTTPRoutePolicies { | ||
| for _, ref := range policy.Spec.TargetRefs { | ||
|
|
@@ -348,28 +348,43 @@ func (t *Translator) fillHTTPRoutePoliciesForHTTPRoute(tctx *provider.TranslateC | |
| } | ||
| } | ||
|
|
||
| t.fillHTTPRoutePolicies(routes, policies) | ||
| return t.fillHTTPRoutePolicies(routes, policies) | ||
| } | ||
|
|
||
| func (t *Translator) fillHTTPRoutePoliciesForIngress(tctx *provider.TranslateContext, routes []*adctypes.Route) { | ||
| t.fillHTTPRoutePolicies(routes, tctx.HTTPRoutePolicies) | ||
| func (t *Translator) fillHTTPRoutePoliciesForIngress(tctx *provider.TranslateContext, routes []*adctypes.Route) error { | ||
| return t.fillHTTPRoutePolicies(routes, tctx.HTTPRoutePolicies) | ||
| } | ||
|
|
||
| func (t *Translator) fillHTTPRoutePolicies(routes []*adctypes.Route, policies []v1alpha1.HTTPRoutePolicy) { | ||
| // fillHTTPRoutePolicies fails on any malformed var: vars are AND-ed match | ||
| // conditions, so dropping one would widen the route. | ||
| func (t *Translator) fillHTTPRoutePolicies(routes []*adctypes.Route, policies []v1alpha1.HTTPRoutePolicy) error { | ||
| for _, policy := range policies { | ||
| vars, err := ParseHTTPRoutePolicyVars(&policy) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, route := range routes { | ||
| route.Priority = policy.Spec.Priority | ||
| for _, data := range policy.Spec.Vars { | ||
| var v []adctypes.StringOrSlice | ||
| if err := json.Unmarshal(data.Raw, &v); err != nil { | ||
| t.Log.Error(err, "failed to unmarshal spec.Vars item to []StringOrSlice", "data", string(data.Raw)) | ||
| // todo: update status | ||
| continue | ||
| } | ||
| route.Vars = append(route.Vars, v) | ||
| } | ||
| route.Vars = append(route.Vars, vars...) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // ParseHTTPRoutePolicyVars decodes spec.vars, failing on the first malformed item. | ||
| func ParseHTTPRoutePolicyVars(policy *v1alpha1.HTTPRoutePolicy) (adctypes.Vars, error) { | ||
| vars := make(adctypes.Vars, 0, len(policy.Spec.Vars)) | ||
| for i, data := range policy.Spec.Vars { | ||
| var v []adctypes.StringOrSlice | ||
| if err := json.Unmarshal(data.Raw, &v); err != nil { | ||
| return nil, fmt.Errorf("HTTPRoutePolicy %s/%s: invalid spec.vars[%d]: %w", policy.Namespace, policy.Name, i, err) | ||
| } | ||
| if len(v) == 0 { | ||
| return nil, fmt.Errorf("HTTPRoutePolicy %s/%s: invalid spec.vars[%d]: empty expression", policy.Namespace, policy.Name, i) | ||
|
Comment on lines
+382
to
+383
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| vars = append(vars, v) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
| return vars, nil | ||
| } | ||
|
|
||
| func (t *Translator) translateEndpointSlice(portName *string, weight int, endpointSlices []discoveryv1.EndpointSlice, endpointFilter func(*discoveryv1.Endpoint) bool) adctypes.UpstreamNodes { | ||
|
|
@@ -806,7 +821,9 @@ func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, httpRou | |
| } | ||
| } | ||
|
|
||
| t.fillHTTPRoutePoliciesForHTTPRoute(tctx, routes, rule) | ||
| if err := t.fillHTTPRoutePoliciesForHTTPRoute(tctx, routes, rule); err != nil { | ||
| return nil, err | ||
| } | ||
| service.Routes = routes | ||
|
|
||
| result.Services = append(result.Services, service) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| package translator | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/go-logr/logr" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| corev1 "k8s.io/api/core/v1" | ||
| networkingv1 "k8s.io/api/networking/v1" | ||
| apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/types" | ||
| gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" | ||
|
|
||
| adctypes "github.com/apache/apisix-ingress-controller/api/adc" | ||
| "github.com/apache/apisix-ingress-controller/api/v1alpha1" | ||
| "github.com/apache/apisix-ingress-controller/internal/provider" | ||
| ) | ||
|
|
||
| const validPolicyVar = `["remote_addr","==","10.0.0.1"]` | ||
|
|
||
| func policyWithVars(vars ...string) v1alpha1.HTTPRoutePolicy { | ||
| policy := v1alpha1.HTTPRoutePolicy{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "policy"}, | ||
| Spec: v1alpha1.HTTPRoutePolicySpec{ | ||
| TargetRefs: []gatewayv1.LocalPolicyTargetReferenceWithSectionName{{ | ||
| LocalPolicyTargetReference: gatewayv1.LocalPolicyTargetReference{ | ||
| Group: gatewayv1.GroupName, | ||
| Kind: "HTTPRoute", | ||
| Name: "route", | ||
| }, | ||
| }}, | ||
| }, | ||
| } | ||
| for _, v := range vars { | ||
| policy.Spec.Vars = append(policy.Spec.Vars, apiextensionsv1.JSON{Raw: []byte(v)}) | ||
| } | ||
| return policy | ||
| } | ||
|
|
||
| func TestParseHTTPRoutePolicyVars(t *testing.T) { | ||
| policy := policyWithVars(validPolicyVar) | ||
| vars, err := ParseHTTPRoutePolicyVars(&policy) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, adctypes.Vars{{{StrVal: "remote_addr"}, {StrVal: "=="}, {StrVal: "10.0.0.1"}}}, vars) | ||
|
|
||
| for _, malformed := range []string{`{"remote_addr":"10.0.0.0/8"}`, `"remote_addr"`, `[{"a":"b"}]`, `[]`, `null`} { | ||
| t.Run(malformed, func(t *testing.T) { | ||
| policy := policyWithVars(validPolicyVar, malformed) | ||
| _, err := ParseHTTPRoutePolicyVars(&policy) | ||
| assert.ErrorContains(t, err, "invalid spec.vars[1]") | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Numbers and booleans keep their JSON type: APISIX compares 18 and "18" differently. | ||
| func TestParseHTTPRoutePolicyVars_Scalars(t *testing.T) { | ||
| policy := policyWithVars(`["arg_age",">",18]`, `["arg_debug","==",true]`, `["arg_id","in",[1,"2"]]`) | ||
| vars, err := ParseHTTPRoutePolicyVars(&policy) | ||
| require.NoError(t, err) | ||
|
|
||
| got, err := json.Marshal(vars) | ||
| require.NoError(t, err) | ||
| assert.JSONEq(t, `[["arg_age",">",18],["arg_debug","==",true],["arg_id","in",[1,"2"]]]`, string(got)) | ||
| } | ||
|
|
||
| func TestTranslateHTTPRoute_HTTPRoutePolicyVars(t *testing.T) { | ||
| pathType := gatewayv1.PathMatchPathPrefix | ||
| pathValue := "/" | ||
| httpRoute := &gatewayv1.HTTPRoute{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "route"}, | ||
| Spec: gatewayv1.HTTPRouteSpec{ | ||
| Rules: []gatewayv1.HTTPRouteRule{{ | ||
| Matches: []gatewayv1.HTTPRouteMatch{{ | ||
| Path: &gatewayv1.HTTPPathMatch{Type: &pathType, Value: &pathValue}, | ||
| }}, | ||
| }}, | ||
| }, | ||
| } | ||
|
|
||
| t.Run("valid vars are applied", func(t *testing.T) { | ||
| tctx := provider.NewDefaultTranslateContext(context.Background()) | ||
| tctx.HTTPRoutePolicies = []v1alpha1.HTTPRoutePolicy{policyWithVars(validPolicyVar)} | ||
|
|
||
| result, err := NewTranslator(logr.Discard(), "").TranslateHTTPRoute(tctx, httpRoute) | ||
| require.NoError(t, err) | ||
| require.Len(t, result.Services, 1) | ||
| require.Len(t, result.Services[0].Routes, 1) | ||
| assert.Contains(t, result.Services[0].Routes[0].Vars, | ||
| []adctypes.StringOrSlice{{StrVal: "remote_addr"}, {StrVal: "=="}, {StrVal: "10.0.0.1"}}) | ||
| }) | ||
|
|
||
| t.Run("malformed var fails translation", func(t *testing.T) { | ||
| tctx := provider.NewDefaultTranslateContext(context.Background()) | ||
| tctx.HTTPRoutePolicies = []v1alpha1.HTTPRoutePolicy{ | ||
| policyWithVars(validPolicyVar, `{"remote_addr":"10.0.0.0/8"}`), | ||
| } | ||
|
|
||
| _, err := NewTranslator(logr.Discard(), "").TranslateHTTPRoute(tctx, httpRoute) | ||
| assert.ErrorContains(t, err, "invalid spec.vars[1]") | ||
| }) | ||
| } | ||
|
|
||
| func TestTranslateIngress_HTTPRoutePolicyMalformedVars(t *testing.T) { | ||
| pathType := networkingv1.PathTypePrefix | ||
| ingress := &networkingv1.Ingress{ | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "ingress"}, | ||
| Spec: networkingv1.IngressSpec{ | ||
| Rules: []networkingv1.IngressRule{{ | ||
| IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{ | ||
| Paths: []networkingv1.HTTPIngressPath{{ | ||
| Path: "/", | ||
| PathType: &pathType, | ||
| Backend: networkingv1.IngressBackend{ | ||
| Service: &networkingv1.IngressServiceBackend{ | ||
| Name: "svc", | ||
| Port: networkingv1.ServiceBackendPort{Number: 80}, | ||
| }, | ||
| }, | ||
| }}, | ||
| }}, | ||
| }}, | ||
| }, | ||
| } | ||
| tctx := &provider.TranslateContext{ | ||
| Services: map[types.NamespacedName]*corev1.Service{ | ||
| {Namespace: "default", Name: "svc"}: { | ||
| ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "svc"}, | ||
| Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 80}}}, | ||
| }, | ||
| }, | ||
| HTTPRoutePolicies: []v1alpha1.HTTPRoutePolicy{ | ||
| policyWithVars(validPolicyVar, `{"remote_addr":"10.0.0.0/8"}`), | ||
| }, | ||
| } | ||
|
|
||
| _, err := NewTranslator(logr.Discard(), "").TranslateIngress(tctx, ingress) | ||
| assert.ErrorContains(t, err, "invalid spec.vars[1]") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Web query:
How does the deployed Apache APISIX version handle a boolean operator in a route vars expression?Result:
Script executed:
Repository: api7/api7-ingress-controller
Length of output: 26005
Script executed:
Repository: api7/api7-ingress-controller
Length of output: 7357
Reject scalar operators in three- and four-element
spec.varsexpressions.ParseHTTPRoutePolicyVarsaccepts a scalar in the operator position. For example,["arg_age",true,18]decodes without error, so the policy is marked accepted. The APISIX expression compiler (lua-resty-expr) lowercases the operator. A boolean operator can fail in that step, and a numeric operator fails with an invalid-operator error. The parser should reject scalars in the operator slot.The operator is the second element of a three-element expression and the third element of a four-element expression, which is
len(v)-2in both cases. Two-element expressions are not checked, because their second element is the value.Suggested fix
if len(v) == 0 { return nil, fmt.Errorf("HTTPRoutePolicy %s/%s: invalid spec.vars[%d]: empty expression", policy.Namespace, policy.Name, i) } + if len(v) >= 3 && v[len(v)-2].ScalarVal != nil { + return nil, fmt.Errorf("HTTPRoutePolicy %s/%s: invalid spec.vars[%d]: operator must be a string", policy.Namespace, policy.Name, i) + } vars = append(vars, v)