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 c95615265 fix(limit-count): validate variable-resolved 
count/time_window bounds (#13573)
c95615265 is described below

commit c95615265b5d34a103113746cde8405fb197f954
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Wed Jun 24 13:27:02 2026 +0800

    fix(limit-count): validate variable-resolved count/time_window bounds 
(#13573)
---
 apisix/plugins/limit-count/init.lua |  29 +++++--
 t/plugin/limit-count-variable.t     | 168 ++++++++++++++++++++++++++++++++++++
 2 files changed, 190 insertions(+), 7 deletions(-)

diff --git a/apisix/plugins/limit-count/init.lua 
b/apisix/plugins/limit-count/init.lua
index ed4db64a8..838bf27fd 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -391,7 +391,19 @@ local function resolve_var(ctx, value)
         end
         value = tonumber(value)
         if not value then
-            return nil, "resolved value is not a number: " .. tostring(value)
+            return nil, "resolved value is not a number"
+        end
+        -- count/time_window must be positive integers, matching the schema
+        if value <= 0 then
+            return nil, "resolved value must be a positive number, got: " .. 
tostring(value)
+        end
+        if value ~= math_floor(value) then
+            return nil, "resolved value must be an integer, got: " .. 
tostring(value)
+        end
+        -- LuaJIT doubles lose integer precision above 2^53
+        if value > 9007199254740991 then
+            return nil, "resolved value exceeds safe integer range (2^53-1), 
got: "
+                        .. tostring(value)
         end
     end
     return value
@@ -420,17 +432,20 @@ local function get_rules(ctx, conf)
 
     local rules = {}
     for index, rule in ipairs(conf.rules) do
+        -- a rule keyed on a var absent for this request just doesn't apply
+        local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)
+        if n_resolved == 0 then
+            goto CONTINUE
+        end
+        -- the rule applies, so an invalid count/time_window must reject, not
+        -- silently skip it, else a client-controlled var could disable 
limiting
         local count, err = resolve_var(ctx, rule.count)
         if err then
-            goto CONTINUE
+            return nil, err
         end
         local time_window, err2 = resolve_var(ctx, rule.time_window)
         if err2 then
-            goto CONTINUE
-        end
-        local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)
-        if n_resolved == 0 then
-            goto CONTINUE
+            return nil, err2
         end
         core.table.insert(rules, {
             count = count,
diff --git a/t/plugin/limit-count-variable.t b/t/plugin/limit-count-variable.t
index a4359c63a..e879a871c 100644
--- a/t/plugin/limit-count-variable.t
+++ b/t/plugin/limit-count-variable.t
@@ -284,3 +284,171 @@ nginx_config:
 --- error_code: 200
 --- access_log eval
 
qr/\{\\x22rate_limiting_key\\x22:\\x22\/apisix\/routes\/1:\d+:test\.com\\x22,\\x22rate_limiting_limit\\x22:2,\\x22rate_limiting_remaining\\x22:1,\\x22rate_limiting_reset\\x22:10}/
+
+
+
+=== TEST 9: set up route with count/time_window from request variables
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "limit-count": {
+                                "count": "${http_count ?? 2}",
+                                "time_window": "${http_time_window ?? 5}",
+                                "rejected_code": 503,
+                                "key_type": "var",
+                                "key": "remote_addr",
+                                "policy": "local"
+                            }
+                        },
+                        "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 10: a client-supplied 0/negative/fractional count is rejected, not 
bypassed
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+            for _, count in ipairs({"0", "-1", "1.5", "9999999999999999"}) do
+                local res = httpc:request_uri(uri, {method = "GET",
+                                                    headers = {["count"] = 
count}})
+                if res.status ~= 500 then
+                    ngx.say("count=", count, " should be rejected with 500, 
got ", res.status)
+                    return
+                end
+            end
+            ngx.say("passed")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- error_log
+resolved value must be a positive number
+resolved value must be an integer
+resolved value exceeds safe integer range
+
+
+
+=== TEST 11: a client-supplied 0/negative/fractional time_window is rejected, 
not bypassed
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+            for _, time_window in ipairs({"0", "-1", "1.5", 
"9999999999999999"}) do
+                local res = httpc:request_uri(uri, {method = "GET",
+                                                    headers = {["time_window"] 
= time_window}})
+                if res.status ~= 500 then
+                    ngx.say("time_window=", time_window, " should be rejected 
with 500, got ",
+                            res.status)
+                    return
+                end
+            end
+            ngx.say("passed")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- error_log
+resolved value must be a positive number
+resolved value must be an integer
+resolved value exceeds safe integer range
+
+
+
+=== TEST 12: set up rules-mode route with count from a request variable
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "limit-count": {
+                                "rejected_code": 503,
+                                "rules": [
+                                    {
+                                        "key": "${http_user}",
+                                        "count": "${http_count ?? 2}",
+                                        "time_window": 60
+                                    }
+                                ]
+                            }
+                        },
+                        "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 13: rules-mode invalid count rejects, not silently skips the rule
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+            -- the rule's key resolves (user header present), so without bounds
+            -- validation a count of 0 would drop the rule and let the request 
pass
+            local res = httpc:request_uri(uri, {method = "GET",
+                                headers = {["user"] = "jack", ["count"] = 
"0"}})
+            if res.status ~= 500 then
+                ngx.say("invalid rule count should be rejected with 500, got 
", res.status)
+                return
+            end
+            ngx.say("passed")
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- error_log
+resolved value must be a positive number

Reply via email to