This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 7f2eaa7c1 feat(hmac-auth): add max_req_body_size to bound request body
during validation (#13478)
7f2eaa7c1 is described below
commit 7f2eaa7c12a2974c78b3c67e3562a72fd2fd9d45
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Jun 9 15:40:27 2026 +0800
feat(hmac-auth): add max_req_body_size to bound request body during
validation (#13478)
---
apisix/plugins/hmac-auth.lua | 28 +++++-
docs/en/latest/plugins/hmac-auth.md | 1 +
t/plugin/hmac-auth3.t | 179 ++++++++++++++++++++++++++++++++++++
3 files changed, 204 insertions(+), 4 deletions(-)
diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua
index 9edd12807..50c0c8ee6 100644
--- a/apisix/plugins/hmac-auth.lua
+++ b/apisix/plugins/hmac-auth.lua
@@ -63,6 +63,16 @@ local schema = {
title = "A boolean value telling the plugin to enable body
validation",
default = false,
},
+ max_req_body_size = {
+ type = "integer",
+ minimum = 1,
+ default = 67108864,
+ description = "maximum request body size in bytes the plugin reads
"
+ .. "into memory to validate the digest when "
+ .. "validate_request_body is true; larger requests are "
+ .. "rejected with 413. Prevents unbounded memory "
+ .. "buffering of large bodies.",
+ },
hide_credentials = {type = "boolean", default = false},
realm = schema_def.get_realm_schema("hmac"),
anonymous_consumer = schema_def.anonymous_consumer_schema,
@@ -255,9 +265,10 @@ local function validate(ctx, conf, params)
return nil, "Invalid digest"
end
- local req_body, err = core.request.get_body()
+ local req_body, err = core.request.get_body(conf.max_req_body_size)
if err then
- return nil, err
+ core.log.error("failed to read request body: ", err)
+ return nil, err, 413
end
req_body = req_body or ""
@@ -318,8 +329,13 @@ local function find_consumer(conf, ctx)
return nil, nil, "client request can't be validated: " .. err
end
- local validated_consumer, err = validate(ctx, conf, params)
+ local validated_consumer, err, status = validate(ctx, conf, params)
if not validated_consumer then
+ if status then
+ -- a definite status code (e.g. 413 for an oversized request body)
+ -- should be returned to the client as-is
+ return nil, nil, err, status
+ end
err = "client request can't be validated: " .. (err or "Invalid
signature")
if auth_utils.is_running_under_multi_auth(ctx) then
return nil, nil, err
@@ -334,8 +350,12 @@ end
function _M.rewrite(conf, ctx)
- local cur_consumer, consumers_conf, err = find_consumer(conf, ctx)
+ local cur_consumer, consumers_conf, err, status = find_consumer(conf, ctx)
if not cur_consumer then
+ if status then
+ -- e.g. 413 when the request body exceeds max_req_body_size
+ return status, { message = err }
+ end
if not conf.anonymous_consumer then
core.response.set_header("WWW-Authenticate", "hmac realm=\"" ..
conf.realm .. "\"")
return 401, { message = err }
diff --git a/docs/en/latest/plugins/hmac-auth.md
b/docs/en/latest/plugins/hmac-auth.md
index 5d9414bce..da2d9486c 100644
--- a/docs/en/latest/plugins/hmac-auth.md
+++ b/docs/en/latest/plugins/hmac-auth.md
@@ -66,6 +66,7 @@ The following attributes are available for configurations on
Routes or Services.
| clock_skew | integer | False | 300
| >=1
| Maximum allowable difference in seconds between the value of
the request's `Date` header (which must be in GMT format) and APISIX's current
time. This helps reject stale requests when the client and server clocks are
reasonably in sync. For the freshness window to be meaningful, `date` must be
part of `signed_heade [...]
| signed_headers | array[string] | False | `["date"]`
|
| The list of headers that must be included in the client
request's HMAC signing string. The default value of `["date"]` ensures the
`Date` header is always bound into the signing string, so that the value used
for the `clock_skew` freshness check is itself covered by the HMAC. Set this
explicitly to override the hea [...]
| validate_request_body | boolean | False | false
|
| If true, validate the integrity of the request body to ensure
it has not been tampered with during transmission. Specifically, the Plugin
creates a SHA-256 base64-encoded digest and compares it to the `Digest` header.
If the `Digest` header is missing or if the digests do not match, the
validation fails. [...]
+| max_req_body_size | integer | False | 67108864
| >= 1
| Maximum request body size in bytes that the Plugin reads into
memory to validate the digest when `validate_request_body` is `true`. Requests
whose body exceeds this limit are rejected with `413`. Prevents unbounded
memory buffering of large request bodies.
[...]
| hide_credentials | boolean | False | false
|
| If true, do not pass the authorization request header to
Upstream services.
[...]
| anonymous_consumer | string | False |
|
| Anonymous Consumer name. If configured, allow anonymous users
to bypass the authentication.
[...]
| realm | string | False | `hmac`
|
| Realm in the
[`WWW-Authenticate`](https://datatracker.ietf.org/doc/html/rfc7235#section-4.1)
response header returned with a `401 Unauthorized` response due to
authentication failure.
[...]
diff --git a/t/plugin/hmac-auth3.t b/t/plugin/hmac-auth3.t
index faeebad99..e744b25ff 100644
--- a/t/plugin/hmac-auth3.t
+++ b/t/plugin/hmac-auth3.t
@@ -278,3 +278,182 @@ qr/\{"message":"client request can't be validated"\}/
}
--- response_body
passed
+
+
+
+=== TEST 6: enable hmac auth with a small max_req_body_size
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "hmac-auth": {
+ "validate_request_body": true,
+ "max_req_body_size": 10
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: request body larger than max_req_body_size is rejected with 413
+--- config
+ location /t {
+ content_by_lua_block {
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_time
+ local core = require("apisix.core")
+ local t = require("lib.test_admin")
+ local hmac = require("resty.hmac")
+ local ngx_encode_base64 = ngx.encode_base64
+
+ local secret_key = "my-secret-key"
+ local timestamp = ngx_time()
+ local gmt = ngx_http_time(timestamp)
+ local key_id = "my-access-key"
+ local custom_header_a = "asld$%dfasf"
+ local custom_header_b = "23879fmsldfk"
+ local body = "{\"name\": \"this body is well over ten bytes\"}"
+
+ local signing_string = {
+ key_id,
+ "POST /hello",
+ "date: " .. gmt,
+ "x-custom-header-a: " .. custom_header_a,
+ "x-custom-header-b: " .. custom_header_b
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+
+ local resty_sha256 = require("resty.sha256")
+ local hash = resty_sha256:new()
+ hash:update(body)
+ local digest = hash:final()
+ local body_digest = ngx_encode_base64(digest)
+
+ local headers = {}
+ headers["Date"] = gmt
+ headers["Digest"] = "SHA-256=" .. body_digest
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date
x-custom-header-a x-custom-header-b\",signature=\"" ..
ngx_encode_base64(signature) .. "\""
+ headers["x-custom-header-a"] = custom_header_a
+ headers["x-custom-header-b"] = custom_header_b
+
+ local code, body = t.test('/hello',
+ ngx.HTTP_POST,
+ body,
+ nil,
+ headers
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- error_code: 413
+--- error_log
+failed to read request body
+
+
+
+=== TEST 8: request body within max_req_body_size is accepted
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "hmac-auth": {
+ "validate_request_body": true,
+ "max_req_body_size": 1024
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ local ngx_time = ngx.time
+ local ngx_http_time = ngx.http_time
+ local core = require("apisix.core")
+ local at = require("lib.test_admin")
+ local hmac = require("resty.hmac")
+ local ngx_encode_base64 = ngx.encode_base64
+
+ local secret_key = "my-secret-key"
+ local timestamp = ngx_time()
+ local gmt = ngx_http_time(timestamp)
+ local key_id = "my-access-key"
+ local custom_header_a = "asld$%dfasf"
+ local custom_header_b = "23879fmsldfk"
+ local req_body = "{\"name\": \"world\"}"
+
+ local signing_string = {
+ key_id,
+ "POST /hello",
+ "date: " .. gmt,
+ "x-custom-header-a: " .. custom_header_a,
+ "x-custom-header-b: " .. custom_header_b
+ }
+ signing_string = core.table.concat(signing_string, "\n") .. "\n"
+
+ local signature = hmac:new(secret_key,
hmac.ALGOS.SHA256):final(signing_string)
+
+ local resty_sha256 = require("resty.sha256")
+ local hash = resty_sha256:new()
+ hash:update(req_body)
+ local digest = hash:final()
+ local body_digest = ngx_encode_base64(digest)
+
+ local headers = {}
+ headers["Date"] = gmt
+ headers["Digest"] = "SHA-256=" .. body_digest
+ headers["Authorization"] = "Signature keyId=\"" .. key_id ..
"\",algorithm=\"hmac-sha256\"" .. ",headers=\"@request-target date
x-custom-header-a x-custom-header-b\",signature=\"" ..
ngx_encode_base64(signature) .. "\""
+ headers["x-custom-header-a"] = custom_header_a
+ headers["x-custom-header-b"] = custom_header_b
+
+ local code, body = at.test('/hello',
+ ngx.HTTP_POST,
+ req_body,
+ nil,
+ headers
+ )
+
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed