feat: add plugin JWT Decoder - #12
Conversation
Signed-off-by: Chiloute <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds a new Spilltea Lua plugin that scans request headers for JWT-looking values, decodes the base64url payload, and exposes the decoded payload by injecting an X-JWT-Decoded-* header into the outbound request (to show up in intercept/history/replay).
Changes:
- Introduces
plugins/jwt_decoder.luaimplementing base64url decoding and JWT payload extraction. - Scans all request headers (optionally stripping a leading
Bearerprefix) and adds decoded payloads back as new request headers.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| B64_DEC[B64_ALPHABET:sub(i, i)] = i - 1 | ||
| end | ||
|
|
||
| -- Decode an unpadded base64url string. Returns the decoded string, or nil if |
| if decoded:match("^%s*{") == nil then | ||
| return nil | ||
| end | ||
| decoded = decoded:gsub("[\r\n]+", " ") |
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
plugins/jwt_decoder.lua:58
decode_jwt_payloaddecodes attacker-controlled header values without any size limits. A large header that matches the JWT shape can cause large allocations (base64 decode + decoded string) and can also create an oversized outboundX-JWT-Decoded-*header that may break forwarding through proxies/servers with header-size limits.
Consider bounding the work and skipping decoding when the token/payload/decoded JSON exceeds a reasonable limit (e.g., 8KB).
local function decode_jwt_payload(value)
-- Three dot-separated base64url segments; the signature may be empty (alg:none).
local payload = value:match("^[A-Za-z0-9_-]+%.([A-Za-z0-9_-]+)%.[A-Za-z0-9_-]*$")
if not payload then
return nil
plugins/jwt_decoder.lua:51
b64url_decodecurrently accepts inputs with non-zero leftover bits (i.e., invalid base64url when unpadded). That can decode malformed payload segments and increases the chance of false positives (or inconsistent decoding) when scanning arbitrary header values.
Consider rejecting inputs where leftover bits remain and are not all zero.
end
return table.concat(out)
end
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
plugins/jwt_decoder.lua:48
b64url_decodeonly emits a single byte whenbits >= 8. Because base64url adds 6 bits per character,bitscan exceed 16 and should emit multiple bytes per iteration; usingifhere produces incorrect decoding for most inputs (and can lead to invalid byte values passed tostring.char).
if bits >= 8 then
bits = bits - 8
out[#out + 1] = string.char(math.floor(acc / 2 ^ bits))
acc = acc % 2 ^ bits
end
plugins/jwt_decoder.lua:59
- The PR description says the signature segment is optional for
alg:none, but the current pattern requires a trailing.(i.e., always 3 segments). If you want to support 2-segment tokens (header.payload) as well, the match needs to allow that form.
-- Three dot-separated base64url segments; the signature may be empty (alg:none).
local payload = value:match("^[A-Za-z0-9_-]+%.([A-Za-z0-9_-]+)%.[A-Za-z0-9_-]*$")
if not payload or #payload > 8192 then
return nil
end
Close #11
What
Adds a plugin (
plugins/jwt_decoder.lua) that decodes JWTs found in request headers.Unlike the original proposal (which only mentioned the
Authorizationheader), thisimplementation scans every request header. For any header value that looks like
a JWT (three dot-separated base64url segments, signature segment optional for
alg: none), the decoded payload is added back to the request as a new header:A leading
Bearerprefix is stripped before detection, so both standard andcustom auth headers are handled.
Details
^%s*{) before being accepted, toavoid false positives on non-JWT dot-separated strings.
The decoded value should not be treated as verified/trusted.
history, and replay views.