This is an automated email from the ASF dual-hosted git repository.

AlinsRan 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 6dbdb9354 fix(jwe-decrypt): reject malformed tokens instead of 
returning 500 (#13844)
6dbdb9354 is described below

commit 6dbdb93541da823ff022fd0ddc091592c44bab5e
Author: AlinsRan <[email protected]>
AuthorDate: Thu Aug 27 17:18:58 2026 +0800

    fix(jwe-decrypt): reject malformed tokens instead of returning 500 (#13844)
---
 apisix/plugins/jwe-decrypt.lua |  30 +++++--
 t/plugin/jwe-decrypt.t         | 178 ++++++++++++++++++++++++++++++++++++-----
 2 files changed, 182 insertions(+), 26 deletions(-)

diff --git a/apisix/plugins/jwe-decrypt.lua b/apisix/plugins/jwe-decrypt.lua
index 9c74aaf5d..bdaee8425 100644
--- a/apisix/plugins/jwe-decrypt.lua
+++ b/apisix/plugins/jwe-decrypt.lua
@@ -19,6 +19,7 @@ local consumer_mod    = require("apisix.consumer")
 local base64          = require("ngx.base64")
 local aes             = require("resty.aes")
 local sub_str         = string.sub
+local type            = type
 local cipher          = aes.cipher(256, "gcm")
 
 local plugin_name     = "jwe-decrypt"
@@ -84,7 +85,12 @@ function _M.check_schema(conf, schema_type)
             -- restrict the length of secret, we use A256GCM for encryption,
             -- so the length should be 32 chars only
             if conf.is_base64_encoded then
-                if #base64.decode_base64url(conf.secret) ~= 32 then
+                local decoded = base64.decode_base64url(conf.secret)
+                if not decoded then
+                    return false, "the secret should be a base64url encoded 
string"
+                end
+
+                if #decoded ~= 32 then
                     return false, "the secret length after base64 decode 
should be 32 chars"
                 end
             else
@@ -122,7 +128,9 @@ local function load_jwe_token(jwe_token)
         return o
     end
     o.header_obj = core.json.decode(he)
-    if not o.header_obj then
+    -- a JSON scalar decodes to a non-table value, `null` even to a truthy
+    -- userdata, so indexing the header later would throw
+    if type(o.header_obj) ~= "table" then
         return o
     end
     o.valid = true
@@ -132,17 +140,27 @@ end
 
 local function jwe_decrypt_with_obj(o, consumer)
     local secret = get_secret(consumer.auth_conf)
+    if not secret then
+        return nil, "invalid secret in the consumer configuration"
+    end
+
     local dec = base64.decode_base64url
+    local iv, ciphertext, tag = dec(o.iv), dec(o.ciphertext), dec(o.tag)
+    if not iv or not ciphertext or not tag then
+        return nil, "invalid base64url encoding in the JWE token"
+    end
 
-    local aes_default = aes:new(
+    local aes_default, err = aes:new(
         secret,
         nil,
         cipher,
-        {iv = dec(o.iv)}
+        {iv = iv}
     )
+    if not aes_default then
+        return nil, err
+    end
 
-    local decrypted, err = aes_default:decrypt(dec(o.ciphertext), dec(o.tag))
-    return decrypted, err
+    return aes_default:decrypt(ciphertext, tag)
 end
 
 
diff --git a/t/plugin/jwe-decrypt.t b/t/plugin/jwe-decrypt.t
index be346ea1d..53f407c98 100644
--- a/t/plugin/jwe-decrypt.t
+++ b/t/plugin/jwe-decrypt.t
@@ -152,7 +152,35 @@ YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZn
 
 
 
-=== TEST 6: add consumer with username and plugins
+=== TEST 6: secret that is not valid base64url is rejected by the schema check
+--- yaml_config
+apisix:
+  data_encryption:
+    enable_encrypt_fields: false
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local plugin = require("apisix.plugins.jwe-decrypt")
+            local ok, err = plugin.check_schema({key = "123", secret = 
"!!!not-base64!!!",
+                                                 is_base64_encoded = true},
+                                                core.schema.TYPE_CONSUMER)
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- response_body
+the secret should be a base64url encoded string
+done
+--- no_error_log
+!!!not-base64!!!
+
+
+
+=== TEST 7: add consumer with username and plugins
 --- config
     location /t {
         content_by_lua_block {
@@ -183,7 +211,7 @@ passed
 
 
 
-=== TEST 7: verify encrypted field
+=== TEST 8: verify encrypted field
 --- config
     location /t {
         content_by_lua_block {
@@ -205,7 +233,7 @@ 
f9pGB0Dt4gYNCLKiINPfVSviKjQs2zfkBCT4+XZ3mDABZkJTr0orzYRD5CptDKMc
 
 
 
-=== TEST 8: enable jwe-decrypt plugin using admin api
+=== TEST 9: enable jwe-decrypt plugin using admin api
 --- config
     location /t {
         content_by_lua_block {
@@ -242,7 +270,7 @@ passed
 
 
 
-=== TEST 9: verify, missing token
+=== TEST 10: verify, missing token
 --- request
 GET /hello
 --- error_code: 403
@@ -251,7 +279,7 @@ GET /hello
 
 
 
-=== TEST 10: verify: invalid JWE token
+=== TEST 11: verify: invalid JWE token
 --- request
 GET /hello
 --- more_headers
@@ -262,7 +290,7 @@ Authorization: 
invalid-eyJraWQiOiJ1c2VyLWtleSIsImFsZyI6ImRpciIsImVuYyI6IkEyNTZHQ
 
 
 
-=== TEST 11: verify (in header)
+=== TEST 12: verify (in header)
 --- request
 GET /hello
 --- more_headers
@@ -272,7 +300,7 @@ hello world
 
 
 
-=== TEST 12: verify (in header without Bearer)
+=== TEST 13: verify (in header without Bearer)
 --- request
 GET /hello
 --- more_headers
@@ -282,7 +310,7 @@ hello world
 
 
 
-=== TEST 13: verify (header with bearer)
+=== TEST 14: verify (header with bearer)
 --- request
 GET /hello
 --- more_headers
@@ -292,7 +320,7 @@ hello world
 
 
 
-=== TEST 14: verify (invalid bearer token)
+=== TEST 15: verify (invalid bearer token)
 --- request
 GET /hello
 --- more_headers
@@ -303,7 +331,7 @@ Authorization: bearer 
invalid-eyJraWQiOiJ1c2VyLWtleSIsImFsZyI6ImRpciIsImVuYyI6Ik
 
 
 
-=== TEST 15: delete a exist consumer
+=== TEST 16: delete a exist consumer
 --- config
     location /t {
         content_by_lua_block {
@@ -362,7 +390,7 @@ code: true body: passed
 
 
 
-=== TEST 16: add consumer with username and plugins with base64 secret
+=== TEST 17: add consumer with username and plugins with base64 secret
 --- config
     location /t {
         content_by_lua_block {
@@ -394,7 +422,7 @@ fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=
 
 
 
-=== TEST 17: enable jwt decrypt plugin with base64 secret
+=== TEST 18: enable jwt decrypt plugin with base64 secret
 --- config
     location /t {
         content_by_lua_block {
@@ -430,7 +458,7 @@ fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=
 
 
 
-=== TEST 18: verify (in header)
+=== TEST 19: verify (in header)
 --- request
 GET /hello
 --- more_headers
@@ -442,7 +470,7 @@ fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=
 
 
 
-=== TEST 19: verify (in header without Bearer)
+=== TEST 20: verify (in header without Bearer)
 --- request
 GET /hello
 --- more_headers
@@ -452,7 +480,7 @@ hello world
 
 
 
-=== TEST 20: enable jwt decrypt plugin with test upstream route
+=== TEST 21: enable jwt decrypt plugin with test upstream route
 --- config
     location /t {
         content_by_lua_block {
@@ -488,7 +516,7 @@ fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=
 
 
 
-=== TEST 21: verify in upstream header
+=== TEST 22: verify in upstream header
 --- request
 GET /headers
 --- more_headers
@@ -500,7 +528,7 @@ fo4XKdZ1xSrIZyms4q2BwPrW5lMpls9qqy5tiAk2esc=
 
 
 
-=== TEST 22: setup route protected by jwe-decrypt
+=== TEST 23: setup route protected by jwe-decrypt
 --- config
     location /t {
         content_by_lua_block {
@@ -553,7 +581,7 @@ done
 
 
 
-=== TEST 23: well-formed token whose ciphertext does not decrypt is rejected
+=== TEST 24: well-formed token whose ciphertext does not decrypt is rejected
 --- config
     location /t {
         content_by_lua_block {
@@ -580,7 +608,7 @@ status: 400
 
 
 
-=== TEST 24: enable jwe-decrypt with strict=false
+=== TEST 25: enable jwe-decrypt with strict=false
 --- config
     location /t {
         content_by_lua_block {
@@ -619,8 +647,118 @@ passed
 
 
 
-=== TEST 25: missing token with strict=false is allowed
+=== TEST 26: missing token with strict=false is allowed
 --- request
 GET /hello-nonstrict
 --- response_body
 hello world
+
+
+
+=== TEST 27: token whose header is JSON null is rejected
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local enc = require("ngx.base64").encode_base64url
+
+            -- JSON null decodes to a truthy userdata, so the header must be
+            -- checked for being an object before reading the kid from it
+            local token = enc("null") .. ".." .. enc("123456789012") .. "."
+                          .. enc("undecryptable") .. "." .. 
enc("0123456789abcdef")
+
+            local code = t('/jwe-decrypt-fail', ngx.HTTP_GET, nil, nil,
+                           { Authorization = "Bearer " .. token })
+            ngx.say("status: ", code)
+        }
+    }
+--- response_body
+status: 400
+
+
+
+=== TEST 28: token whose header is a JSON scalar is rejected
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local enc = require("ngx.base64").encode_base64url
+
+            local token = enc("123") .. ".." .. enc("123456789012") .. "."
+                          .. enc("undecryptable") .. "." .. 
enc("0123456789abcdef")
+
+            local code = t('/jwe-decrypt-fail', ngx.HTTP_GET, nil, nil,
+                           { Authorization = "Bearer " .. token })
+            ngx.say("status: ", code)
+        }
+    }
+--- response_body
+status: 400
+
+
+
+=== TEST 29: token with invalid base64url in iv, ciphertext and tag is rejected
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local core = require("apisix.core")
+            local enc = require("ngx.base64").encode_base64url
+
+            local header = enc(core.json.encode({
+                alg = "dir", enc = "A256GCM", kid = "jwe-fail-key",
+            }))
+            local token = header .. "..!!!.!!!.!!!"
+
+            local code = t('/jwe-decrypt-fail', ngx.HTTP_GET, nil, nil,
+                           { Authorization = "Bearer " .. token })
+            ngx.say("status: ", code)
+        }
+    }
+--- response_body
+status: 400
+
+
+
+=== TEST 30: consumer secret that is not valid base64url is rejected at runtime
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local core = require("apisix.core")
+            local enc = require("ngx.base64").encode_base64url
+
+            -- data encryption is enabled by default, so the schema check
+            -- cannot validate the secret and the request has to be rejected
+            local code = t('/apisix/admin/consumers',
+                ngx.HTTP_PUT,
+                [[{
+                    "username": "jwe_bad_secret_user",
+                    "plugins": {
+                        "jwe-decrypt": {
+                            "key": "jwe-bad-secret-key",
+                            "secret": "!!!not-base64!!!",
+                            "is_base64_encoded": true
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("failed to add consumer")
+                return
+            end
+
+            local header = enc(core.json.encode({
+                alg = "dir", enc = "A256GCM", kid = "jwe-bad-secret-key",
+            }))
+            local token = header .. ".." .. enc("123456789012") .. "."
+                          .. enc("undecryptable") .. "." .. 
enc("0123456789abcdef")
+
+            code = t('/jwe-decrypt-fail', ngx.HTTP_GET, nil, nil,
+                     { Authorization = "Bearer " .. token })
+            ngx.say("status: ", code)
+        }
+    }
+--- response_body
+status: 400

Reply via email to