From 871c7603b61a23523c734d9f13a74543f9eac109 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 11 Jun 2026 13:05:18 +0800 Subject: [PATCH 1/2] fix(jwt-auth): reject malformed JWT signature instead of erroring verify_signature indexed the per-algorithm verifier and passed the decoded signature without guarding either. A token carrying an unsupported alg, a non-base64url signature, or a signature of the wrong length made the verifier raise a Lua error (e.g. "Signature must be 64 bytes." or a length-of-nil error), which propagated as a 500 response instead of a clean 401 for any route protected by jwt-auth. Guard the algorithm lookup and the base64url decode, and run the verifier under pcall so a malformed token is rejected as an invalid signature rather than crashing the request. --- apisix/plugins/jwt-auth/parser.lua | 22 +++++++- t/plugin/jwt-auth.t | 84 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index 098a26825dcf..da074b484cae 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -223,8 +223,26 @@ end function _M.verify_signature(self, key) - return alg_verify[self.header.alg](self.raw_header .. "." .. - self.raw_payload, base64_decode(self.signature), key) + local verifier = alg_verify[self.header.alg] + if not verifier then + return false, "unsupported algorithm: " .. tostring(self.header.alg) + end + + local signature = base64_decode(self.signature) + if not signature then + return false, "failed to decode signature" + end + + -- the per-algorithm verifiers assert on signature length and key validity, + -- so guard with pcall to turn a malformed token into a clean rejection + -- instead of letting the error propagate as a 500 response + local ok, verified = pcall(verifier, self.raw_header .. "." .. self.raw_payload, + signature, key) + if not ok then + return false, verified + end + + return verified end diff --git a/t/plugin/jwt-auth.t b/t/plugin/jwt-auth.t index f9cb66ec46d9..dc183794b80b 100644 --- a/t/plugin/jwt-auth.t +++ b/t/plugin/jwt-auth.t @@ -1428,3 +1428,87 @@ GET /hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4 {"message":"failed to verify jwt"} --- error_log failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT + + + +=== TEST 59: malformed signature (wrong length) must be rejected, not crash +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- ES256 consumer; the per-algorithm verifier asserts the signature + -- is 64 bytes, so a malformed signature used to raise a Lua error + -- and surface as a 500 instead of a clean 401. + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "kerouac", + "plugins": { + "jwt-auth": { + "key": "user-key-es256", + "algorithm": "ES256", + "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----" + } + } + }]] + ) + assert(code < 300, body) + + code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "jwt-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + assert(code < 300, body) + + -- valid ES256 header + payload (key claim = user-key-es256), but the + -- signature "YWJj" decodes to 3 bytes instead of the required 64 + local token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" + .. ".eyJrZXkiOiJ1c2VyLWtleS1lczI1NiIsIm5iZiI6MTcyNzI3NDk4M30" + .. ".YWJj" + + code, body = t('/hello?jwt=' .. token, ngx.HTTP_GET) + ngx.status = code + ngx.print(body) + } + } +--- error_code: 401 +--- response_body +{"message":"failed to verify jwt"} +--- no_error_log +[error] + + + +=== TEST 60: non-base64url signature must be rejected, not crash +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- reuse the ES256 consumer/route from the previous test; the + -- signature "@@@@" is not valid base64url, so decoding returns nil + -- and used to raise a Lua error when indexed for its length + local token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" + .. ".eyJrZXkiOiJ1c2VyLWtleS1lczI1NiIsIm5iZiI6MTcyNzI3NDk4M30" + .. ".@@@@" + + local code, body = t('/hello?jwt=' .. token, ngx.HTTP_GET) + ngx.status = code + ngx.print(body) + } + } +--- error_code: 401 +--- response_body +{"message":"failed to verify jwt"} +--- no_error_log +[error] From ed14a51181823aa73b8dc5b8e4e6e63c818a0ac7 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 11 Jun 2026 16:32:25 +0800 Subject: [PATCH 2/2] fix(jwt-auth): localize tostring, preserve verifier err, merge tests - localize tostring to satisfy the lj-releng global check (CI lint) - keep the verifier's own (verified, err) return values through the pcall wrapper instead of dropping the secondary error detail - merge the two malformed-signature cases into one self-contained test to remove the order dependency between them --- apisix/plugins/jwt-auth/parser.lua | 9 +++-- t/plugin/jwt-auth.t | 56 +++++++++--------------------- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/apisix/plugins/jwt-auth/parser.lua b/apisix/plugins/jwt-auth/parser.lua index da074b484cae..0d2be383c6c9 100644 --- a/apisix/plugins/jwt-auth/parser.lua +++ b/apisix/plugins/jwt-auth/parser.lua @@ -32,6 +32,7 @@ local ipairs = ipairs local type = type local error = error local pcall = pcall +local tostring = tostring local default_claims = { "nbf", @@ -236,13 +237,15 @@ function _M.verify_signature(self, key) -- the per-algorithm verifiers assert on signature length and key validity, -- so guard with pcall to turn a malformed token into a clean rejection -- instead of letting the error propagate as a 500 response - local ok, verified = pcall(verifier, self.raw_header .. "." .. self.raw_payload, - signature, key) + local ok, verified, verify_err = pcall(verifier, + self.raw_header .. "." .. self.raw_payload, signature, key) if not ok then + -- verifier raised: `verified` holds the caught error message return false, verified end - return verified + -- preserve the verifier's own (verified, err) return contract + return verified, verify_err end diff --git a/t/plugin/jwt-auth.t b/t/plugin/jwt-auth.t index dc183794b80b..29eb6b869201 100644 --- a/t/plugin/jwt-auth.t +++ b/t/plugin/jwt-auth.t @@ -1431,14 +1431,14 @@ failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT -=== TEST 59: malformed signature (wrong length) must be rejected, not crash +=== TEST 59: malformed signatures must be rejected with 401, not crash with 500 --- config location /t { content_by_lua_block { local t = require("lib.test_admin").test - -- ES256 consumer; the per-algorithm verifier asserts the signature - -- is 64 bytes, so a malformed signature used to raise a Lua error - -- and surface as a 500 instead of a clean 401. + -- ES256 consumer: the per-algorithm verifier asserts the signature + -- length and decodes it from base64url, so a malformed signature + -- used to raise a Lua error and surface as a 500 instead of a 401. local code, body = t('/apisix/admin/consumers', ngx.HTTP_PUT, [[{ @@ -1471,44 +1471,22 @@ failed to verify jwt: 'exp' claim expired at Tue, 23 Jul 2019 08:28:21 GMT ) assert(code < 300, body) - -- valid ES256 header + payload (key claim = user-key-es256), but the - -- signature "YWJj" decodes to 3 bytes instead of the required 64 - local token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" + -- valid ES256 header + payload (key claim = user-key-es256), with two + -- malformed signatures: "YWJj" decodes to 3 bytes (not the required + -- 64), and "@@@@" is not valid base64url so decoding returns nil + local header_payload = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" .. ".eyJrZXkiOiJ1c2VyLWtleS1lczI1NiIsIm5iZiI6MTcyNzI3NDk4M30" - .. ".YWJj" - - code, body = t('/hello?jwt=' .. token, ngx.HTTP_GET) - ngx.status = code - ngx.print(body) - } - } ---- error_code: 401 ---- response_body -{"message":"failed to verify jwt"} ---- no_error_log -[error] - - - -=== TEST 60: non-base64url signature must be rejected, not crash ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - -- reuse the ES256 consumer/route from the previous test; the - -- signature "@@@@" is not valid base64url, so decoding returns nil - -- and used to raise a Lua error when indexed for its length - local token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9" - .. ".eyJrZXkiOiJ1c2VyLWtleS1lczI1NiIsIm5iZiI6MTcyNzI3NDk4M30" - .. ".@@@@" - - local code, body = t('/hello?jwt=' .. token, ngx.HTTP_GET) - ngx.status = code - ngx.print(body) + for _, sig in ipairs({"YWJj", "@@@@"}) do + local rc, rb = t('/hello?jwt=' .. header_payload .. "." .. sig, + ngx.HTTP_GET) + assert(rc == 401, "signature '" .. sig .. "' expected 401 but got " + .. tostring(rc)) + assert(string.find(rb, "failed to verify jwt", 1, true), rb) + end + ngx.say("passed") } } ---- error_code: 401 --- response_body -{"message":"failed to verify jwt"} +passed --- no_error_log [error]