Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
needs:
- draft-release
env:
X_GO_DISTRIBUTION: "https://go.dev/dl/go1.24.10.linux-amd64.tar.gz"
X_GO_DISTRIBUTION: "https://go.dev/dl/go1.24.11.linux-amd64.tar.gz"
APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall"
strategy:
matrix:
Expand Down Expand Up @@ -162,7 +162,7 @@ jobs:
needs:
- draft-release
env:
X_GO_VERSION: "1.24.10"
X_GO_VERSION: "1.24.11"
APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall"
strategy:
matrix:
Expand Down Expand Up @@ -272,19 +272,19 @@ jobs:
include:
- arch: armv6
distro: bookworm
go_distribution: https://go.dev/dl/go1.24.10.linux-armv6l.tar.gz
go_distribution: https://go.dev/dl/go1.24.11.linux-armv6l.tar.gz
artifact: armv6-libc
- arch: aarch64
distro: bookworm
go_distribution: https://go.dev/dl/go1.24.10.linux-arm64.tar.gz
go_distribution: https://go.dev/dl/go1.24.11.linux-arm64.tar.gz
artifact: arm64-libc
- arch: armv6
distro: alpine_latest
go_distribution: https://go.dev/dl/go1.24.10.linux-armv6l.tar.gz
go_distribution: https://go.dev/dl/go1.24.11.linux-armv6l.tar.gz
artifact: armv6-musl
- arch: aarch64
distro: alpine_latest
go_distribution: https://go.dev/dl/go1.24.10.linux-arm64.tar.gz
go_distribution: https://go.dev/dl/go1.24.11.linux-arm64.tar.gz
artifact: arm64-musl
steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION := 0.9.4
VERSION := 0.9.5
NAMESPACE := github.com/wallarm/api-firewall

.DEFAULT_GOAL := build
Expand Down
2 changes: 1 addition & 1 deletion demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.8"
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
environment:
APIFW_URL: "http://0.0.0.0:8080"
Expand Down
2 changes: 1 addition & 1 deletion demo/docker-compose/docker-compose-api-mode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.8'
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
environment:
APIFW_MODE: "api"
Expand Down
2 changes: 1 addition & 1 deletion demo/docker-compose/docker-compose-graphql-mode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.8'
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
environment:
APIFW_MODE: "graphql"
Expand Down
2 changes: 1 addition & 1 deletion demo/docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: "3.8"
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
environment:
APIFW_URL: "http://0.0.0.0:8080"
Expand Down
2 changes: 1 addition & 1 deletion demo/kubernetes/volumes/helm/api-firewall.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ manifest:
"url": "https://kennethreitz.org",
"email": "me@kennethreitz.org"
},
"version": "0.9.4"
"version": "0.9.5"
},
"servers": [
{
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration-guides/allowlist.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ docker run --rm -it --network api-firewall-network --network-alias api-firewall
-e APIFW_URL=<API_FIREWALL_URL> -e APIFW_SERVER_URL=<PROTECTED_APP_URL> \
-e APIFW_REQUEST_VALIDATION=<REQUEST_VALIDATION_MODE> -e APIFW_RESPONSE_VALIDATION=<RESPONSE_VALIDATION_MODE> \
-e APIFW_ALLOW_IP_FILE=/opt/ip-allowlist.txt -e APIFW_ALLOW_IP_HEADER_NAME="X-Real-IP" \
-p 8088:8088 wallarm/api-firewall:v0.9.4
-p 8088:8088 wallarm/api-firewall:v0.9.5
```

| Environment variable | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs/installation-guides/api-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Use the following command to run the API Firewall container:

```
docker run --rm -it -v <PATH_TO_SQLITE_DATABASE>:/var/lib/wallarm-api/1/wallarm_api.db \
-e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.4
-e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.5
```

You can pass to the container the following variables:
Expand Down
4 changes: 2 additions & 2 deletions docs/installation-guides/docker-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ networks:
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
volumes:
- <HOST_PATH_TO_SPEC>:<CONTAINER_PATH_TO_SPEC>
Expand Down Expand Up @@ -171,6 +171,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in
-v <HOST_PATH_TO_SPEC>:<CONTAINER_PATH_TO_SPEC> -e APIFW_API_SPECS=<PATH_TO_MOUNTED_SPEC> \
-e APIFW_URL=<API_FIREWALL_URL> -e APIFW_SERVER_URL=<PROTECTED_APP_URL> \
-e APIFW_REQUEST_VALIDATION=<REQUEST_VALIDATION_MODE> -e APIFW_RESPONSE_VALIDATION=<RESPONSE_VALIDATION_MODE> \
-p 8088:8088 wallarm/api-firewall:v0.9.4
-p 8088:8088 wallarm/api-firewall:v0.9.5
```
4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7.
4 changes: 2 additions & 2 deletions docs/installation-guides/graphql/docker-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ networks:
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
restart: on-failure
volumes:
- <HOST_PATH_TO_SPEC>:<CONTAINER_PATH_TO_SPEC>
Expand Down Expand Up @@ -200,6 +200,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in
-e APIFW_GRAPHQL_MAX_QUERY_COMPLEXITY=<MAX_QUERY_COMPLEXITY> \
-e APIFW_GRAPHQL_MAX_QUERY_DEPTH=<MAX_QUERY_DEPTH> -e APIFW_GRAPHQL_NODE_COUNT_LIMIT=<NODE_COUNT_LIMIT> \
-e APIFW_GRAPHQL_INTROSPECTION=<ALLOW_INTROSPECTION_OR_NOT> \
-p 8088:8088 wallarm/api-firewall:v0.9.4
-p 8088:8088 wallarm/api-firewall:v0.9.5
```
4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7.
5 changes: 5 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This page describes new releases of Wallarm API Firewall.

## v0.9.5 (2025-12-05)

* Upgrade Go to 1.24.11
* Fix the unknown parameters detection issue

## v0.9.4 (2025-11-28)

* Upgrade Go to 1.24.10
Expand Down
2 changes: 1 addition & 1 deletion helm/api-firewall/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v1
name: api-firewall
version: 0.7.2
appVersion: 0.9.4
appVersion: 0.9.5
description: Wallarm OpenAPI-based API Firewall
home: https://github.com/wallarm/api-firewall
icon: https://static.wallarm.com/wallarm-logo.svg
Expand Down
75 changes: 62 additions & 13 deletions internal/platform/validator/unknown_parameters_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ var ErrUnknownBodyParameter = errors.New("body parameter not defined in the Open
// ErrDecodingFailed is returned when the API FW got error or unexpected value from the decoder
var ErrDecodingFailed = errors.New("the decoder returned the error")

// ErrMaxDepthExceeded is returned when the JSON body exceeds the maximum allowed nesting depth
var ErrMaxDepthExceeded = errors.New("maximum JSON nesting depth exceeded")

const (
// maxJSONArrayDepth is the maximum allowed nesting depth for JSON arrays
maxJSONArrayDepth = 32
// maxJSONArrayElements is the maximum number of array elements to process
maxJSONArrayElements = 10000
)

// RequestParameterDetails contains details about found unknown parameter
type RequestParameterDetails struct {
Name string `json:"name"`
Expand Down Expand Up @@ -74,6 +84,50 @@ func identifyData(data any) string {
return "unknown"
}

// findUnknownParamsInJSONBody searches for unknown parameters in the body.
// The function covers array of objects and single object.
// The depth parameter limits recursion to prevent stack overflow from deeply nested arrays.
func findUnknownParamsInJSONBody(decodedBody any, contentType *openapi3.MediaType, depth int) ([]RequestParameterDetails, error) {
if depth > maxJSONArrayDepth {
return nil, ErrMaxDepthExceeded
}

var unknownParameters []RequestParameterDetails

paramLists, ok := decodedBody.([]interface{})
if ok {
// Limit the number of array elements to process
if len(paramLists) > maxJSONArrayElements {
paramLists = paramLists[:maxJSONArrayElements]
}
for _, paramList := range paramLists {
currentUnknownParameters, err := findUnknownParamsInJSONBody(paramList, contentType, depth+1)
if err != nil {
return nil, err
}
unknownParameters = append(unknownParameters, currentUnknownParameters...)
}
return unknownParameters, nil
}

paramList, ok := decodedBody.(map[string]any)
if !ok {
return unknownParameters, ErrDecodingFailed
}

for paramName := range paramList {
if _, found := contentType.Schema.Value.Properties[paramName]; !found {
unknownParameters = append(unknownParameters, RequestParameterDetails{
Name: paramName,
Placeholder: "body",
Type: identifyData(paramList[paramName]),
})
}
}

return unknownParameters, nil
}

// ValidateUnknownRequestParameters is used to get a list of request parameters that are not specified in the OpenAPI specification
func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.Route, header http.Header, jsonParser *fastjson.Parser) (foundUnknownParams []RequestUnknownParameterError, valError error) {

Expand Down Expand Up @@ -193,21 +247,16 @@ func ValidateUnknownRequestParameters(ctx *fasthttp.RequestCtx, route *routers.R
}
})
case mType == "application/json" || mType == "multipart/form-data" || suffix == "+json":
paramList, ok := value.(map[string]any)
if !ok {
return foundUnknownParams, nil
}

for paramName := range paramList {
if _, found := contentType.Schema.Value.Properties[paramName]; !found {
unknownBodyParams.Message = ErrUnknownBodyParameter.Error()
unknownBodyParams.Parameters = append(unknownBodyParams.Parameters, RequestParameterDetails{
Name: paramName,
Placeholder: "body",
Type: identifyData(paramList[paramName]),
})
}
currentUBParams, err := findUnknownParamsInJSONBody(value, contentType, 0)
if err != nil {
return foundUnknownParams, err
}
if len(currentUBParams) > 0 {
unknownBodyParams.Message = ErrUnknownBodyParameter.Error()
unknownBodyParams.Parameters = append(unknownBodyParams.Parameters, currentUBParams...)
}

case mType == "application/xml" || suffix == "+xml":
var propKeys []string
for key := range contentType.Schema.Value.Properties {
Expand Down
79 changes: 78 additions & 1 deletion internal/platform/validator/unknown_parameters_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -116,6 +117,7 @@ paths:
requestBody *testRequestBody
ct string
url string
array bool
}
tests := []struct {
name string
Expand Down Expand Up @@ -219,6 +221,26 @@ paths:
},
},
},
{
name: "Unknown JSON param in array",
args: args{
requestBody: &testRequestBody{SubCategory: "Chocolate", Category: &categoryFood, UnknownParameter: "test"},
url: "/category?category=cookies",
ct: "application/json",
array: true,
},
expectedErr: nil,
expectedResp: []*RequestUnknownParameterError{
{
Parameters: []RequestParameterDetails{{
Name: "unknown",
Placeholder: "body",
Type: "string",
}},
Message: ErrUnknownBodyParameter.Error(),
},
},
},
{
name: "Unknown POST param",
args: args{
Expand Down Expand Up @@ -399,7 +421,14 @@ paths:
}
requestBody = strings.NewReader(req.PostArgs().String())
case "application/json":
testingBody, err := json.Marshal(tc.args.requestBody)
var err error
var testingBody []byte
if tc.args.array {
testingBody, err = json.Marshal([]testRequestBody{*tc.args.requestBody})
} else {
testingBody, err = json.Marshal(tc.args.requestBody)
}

require.NoError(t, err)
requestBody = bytes.NewReader(testingBody)
case "application/xml":
Expand Down Expand Up @@ -513,3 +542,51 @@ func matchUnknownParamsResp(expected []*RequestUnknownParameterError, actual []R

return true
}

func TestFindUnknownParamsInJSONBody_DepthLimit(t *testing.T) {
schema := &openapi3.MediaType{
Schema: &openapi3.SchemaRef{
Value: &openapi3.Schema{
Properties: openapi3.Schemas{
"field": &openapi3.SchemaRef{
Value: &openapi3.Schema{Type: &openapi3.Types{"string"}},
},
},
},
},
}

// Create deeply nested array that exceeds maxJSONArrayDepth (32)
var nested any = map[string]any{"field": "value"}
for i := 0; i < 35; i++ {
nested = []interface{}{nested}
}

_, err := findUnknownParamsInJSONBody(nested, schema, 0)
assert.ErrorIs(t, err, ErrMaxDepthExceeded, "expected ErrMaxDepthExceeded for deeply nested array")
}

func TestFindUnknownParamsInJSONBody_ElementLimit(t *testing.T) {
schema := &openapi3.MediaType{
Schema: &openapi3.SchemaRef{
Value: &openapi3.Schema{
Properties: openapi3.Schemas{
"field": &openapi3.SchemaRef{
Value: &openapi3.Schema{Type: &openapi3.Types{"string"}},
},
},
},
},
}

// Create array with more than maxJSONArrayElements (10000) elements
largeArray := make([]interface{}, 15000)
for i := 0; i < 15000; i++ {
largeArray[i] = map[string]any{"unknown": "value"}
}

result, err := findUnknownParamsInJSONBody(largeArray, schema, 0)
require.NoError(t, err)
// Should only process maxJSONArrayElements (10000) elements
assert.LessOrEqual(t, len(result), maxJSONArrayElements, "should limit processed elements to maxJSONArrayElements")
}
2 changes: 1 addition & 1 deletion resources/test/docker-compose-api-mode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.8'
services:
api-firewall:
container_name: api-firewall
image: wallarm/api-firewall:v0.9.4
image: wallarm/api-firewall:v0.9.5
build:
context: ../../
dockerfile: Dockerfile
Expand Down
Loading