MINOR: jwt: Improve 'jwt_tokenize' function

The 'jwt_tokenize' function that can be used to split a JWT token into
its subparts can either fully process the token (from beginning to end)
when we need to check its signature, or only partially when using the
jwt_header_query or jwt_member_query converters. In this case we relied
on the fact that the return value of the 'jwt_tokenize' function was not
checked because a '-1' was returned (which was not actually an error).

In order to make this logic more explicit, the 'jwt_tokenize' function
now has a way to warn the caller that the token was invalid (less
subparts than the specified 'item_num') or that the token was not
processed in full (enough subparts found without parsing the token all
the way).
The function will now only return 0 if we found strictly the same number
of subparts as 'item_num'.
This commit is contained in:
Remi Tricot-Le Breton 2026-03-10 14:11:55 +01:00 committed by William Lallemand
parent 1babe8cb1b
commit e34b633be3
5 changed files with 68 additions and 25 deletions

View File

@ -27,7 +27,7 @@
#ifdef USE_OPENSSL
enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len);
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num);
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int item_num);
int jwt_tree_load_cert(char *path, int pathlen, int tryload_cert, const char *file, int line, char **err);
enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg,

View File

@ -16,7 +16,7 @@ feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL)'"
feature cmd "command -v socat"
feature ignore_unknown_macro
server s1 -repeat 27 {
server s1 -repeat 40 {
rxreq
txresp
} -start
@ -542,3 +542,44 @@ client c27 -connect ${h1_mainfe_sock} {
expect resp.http.x-jwt-verify-RS256-var2 == "1"
} -run
client c28 -connect ${h1_mainfe_sock} {
# Token content : {"alg":"none"}
# {"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
txreq -url "/none" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
rxresp
expect resp.status == 200
expect resp.http.x-jwt-alg == "none"
expect resp.http.x-jwt-verify == "1"
} -run
client c29 -connect ${h1_mainfe_sock} {
# Invalid Token : too many subparts
txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa.aa.aa"
rxresp
expect resp.status == 200
expect resp.http.x-jwt-alg == "none"
expect resp.http.x-jwt-verify == "-3"
# Invalid Token : too many subparts
txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa.aa."
rxresp
expect resp.status == 200
expect resp.http.x-jwt-alg == "none"
expect resp.http.x-jwt-verify == "-3"
# Invalid Token : too few subparts
txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJub25lIn0.aa"
rxresp
expect resp.status == 200
expect resp.http.x-jwt-alg == "none"
expect resp.http.x-jwt-verify == "-3"
# Invalid Token : no signature but alg different than "none"
txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
rxresp
expect resp.status == 200
expect resp.http.x-jwt-alg == "RS256"
expect resp.http.x-jwt-verify == "-3"
} -run

View File

@ -534,7 +534,7 @@ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample
if (!chunk_cpy(input, &smp->data.u.str))
goto end;
if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
if (jwt_tokenize(input, items, item_num))
goto end;
alg_tag = alloc_trash_chunk();
@ -789,7 +789,7 @@ static int sample_conv_jwt_decrypt_cert(const struct arg *args, struct sample *s
if (!chunk_cpy(input, &smp->data.u.str))
goto end;
if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
if (jwt_tokenize(input, items, item_num))
goto end;
/* Base64Url decode the JOSE header */
@ -1302,7 +1302,7 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm
if (!chunk_cpy(input, &smp->data.u.str))
goto end;
if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
if (jwt_tokenize(input, items, item_num))
goto end;
alg_tag = alloc_trash_chunk();

View File

@ -94,26 +94,30 @@ enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len)
* now, we don't need to manage more than three subparts in the tokens.
* See section 3.1 of RFC7515 for more information about JWS Compact
* Serialization.
* Returns 0 in case of success.
* Returns -1 in case of error, 0 if the token has exactly <item_num> parts, a
* positive value otherwise.
*/
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num)
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int item_num)
{
char *ptr = jwt->area;
char *jwt_end = jwt->area + jwt->data;
unsigned int index = 0;
unsigned int length = 0;
if (index < *item_num) {
items[index].start = ptr;
items[index].length = 0;
}
if (item_num == 0)
return -1;
while (index < *item_num && ptr < jwt_end) {
items[index].start = ptr;
items[index].length = 0;
while (ptr < jwt_end) {
if (*ptr++ == '.') {
items[index++].length = length;
/* We found enough items, no need to keep looking for
* separators. */
if (index == item_num)
return 1;
if (index == *item_num)
return -1;
items[index].start = ptr;
items[index].length = 0;
length = 0;
@ -121,10 +125,11 @@ int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int
++length;
}
if (index < *item_num)
items[index].length = length;
/* We might not have found enough items */
if (index < item_num - 1)
return -1;
*item_num = (index+1);
items[index].length = length;
return (ptr != jwt_end);
}
@ -493,13 +498,9 @@ enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer
if (ctx.alg == JWT_ALG_DEFAULT)
return JWT_VRFY_UNKNOWN_ALG;
if (jwt_tokenize(token, items, &item_num))
if (jwt_tokenize(token, items, item_num))
return JWT_VRFY_INVALID_TOKEN;
if (item_num != JWT_ELT_MAX)
if (ctx.alg != JWS_ALG_NONE || item_num != JWT_ELT_SIG)
return JWT_VRFY_INVALID_TOKEN;
ctx.jose = items[JWT_ELT_JOSE];
ctx.claims = items[JWT_ELT_CLAIMS];
ctx.signature = items[JWT_ELT_SIG];

View File

@ -4795,9 +4795,10 @@ static int sample_conv_jwt_member_query(const struct arg *args, struct sample *s
int retval = 0;
int ret;
jwt_tokenize(&smp->data.u.str, items, &item_num);
if (item_num < member + 1)
/* We don't need to extract all the parts from the token, we only need a
* specific one.
*/
if (jwt_tokenize(&smp->data.u.str, items, item_num) < 0)
goto end;
decoded_header = get_trash_chunk_sz(items[member].length);