Hi.
As outcome from that discussion on the ML have I looked into the mjson source.
https://www.mail-archive.com/[email protected]/msg46546.html
There are some small changes and some risky one, IMHO.
Attached the patch series.
Regards
Aleks
From a5824eaf09bb163b532bea31bf9f617d4b876a00 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 14/14] TEST/MEDIUM: tests: extend json_query coverage
Extend converter coverage with malformed-number and ACME-like payloads.
The added cases check multi-digit array indexes, invalid numeric input, and
realistic ACME response fields such as newNonce, authorizations, and error
type values. This helps guard the mjson hardening changes against regressions.
---
reg-tests/converter/json_query.vtc | 46 +++++++++++++++++++++++++++++-
1 file changed, 45 insertions(+), 1 deletion(-)
diff --git a/reg-tests/converter/json_query.vtc b/reg-tests/converter/json_query.vtc
index 087f7833bd..37a51ed5ed 100644
--- a/reg-tests/converter/json_query.vtc
+++ b/reg-tests/converter/json_query.vtc
@@ -5,7 +5,7 @@ feature ignore_unknown_macro
server s1 {
rxreq
txresp -hdr "Connection: close"
-} -repeat 8 -start
+} -repeat 17 -start
haproxy h1 -conf {
global
@@ -37,6 +37,12 @@ haproxy h1 -conf {
http-request set-var(sess.pay_boolean_true) req.body,json_query('$.boolean-true')
http-request set-var(sess.pay_boolean_false) req.body,json_query('$.boolean-false')
http-request set-var(sess.pay_mykey) req.body,json_query('$.my\\.key')
+ http-request set-var(txn.pay_idx12) req.body,json_query('$.items[12]')
+ http-request set-var(txn.pay_idx13) req.body,json_query('$.items[13]')
+ http-request set-var(txn.pay_bad_int) req.body,json_query('$.integer',"int"),add(1)
+ http-request set-var(txn.acme_nonce) req.body,json_query('$.newNonce')
+ http-request set-var(txn.acme_order12) req.body,json_query('$.authorizations[12]')
+ http-request set-var(txn.acme_err_type) req.body,json_query('$.error.type')
http-response set-header x-var_header %[var(sess.header_json)]
http-response set-header x-var_body %[var(sess.pay_json)]
@@ -46,6 +52,12 @@ haproxy h1 -conf {
http-response set-header x-var_body_boolean_true %[var(sess.pay_boolean_true)]
http-response set-header x-var_body_boolean_false %[var(sess.pay_boolean_false)]
http-response set-header x-var_body_mykey %[var(sess.pay_mykey)]
+ http-response set-header x-var_body_idx12 %[var(txn.pay_idx12)]
+ http-response set-header x-var_body_idx13 %[var(txn.pay_idx13)]
+ http-response set-header x-var_body_bad_int %[var(txn.pay_bad_int)]
+ http-response set-header x-var_acme_nonce %[var(txn.acme_nonce)]
+ http-response set-header x-var_acme_order12 %[var(txn.acme_order12)]
+ http-response set-header x-var_acme_err_type %[var(txn.acme_err_type)]
default_backend be
@@ -104,7 +116,39 @@ client c1 -connect ${h1_fe_sock} {
txreq -url "/" \
-body "{\"my.key\":[\"val1\",\"val2\",\"val3\"],\"key2\":\"val4\"}"
+ rxresp
expect resp.status == 200
expect resp.http.x-var_body_mykey ~ "[\"val1\",\"val2\",\"val3\"]"
+ txreq -url "/" \
+ -body "{\"items\":[\"v0\",\"v1\",\"v2\",\"v3\",\"v4\",\"v5\",\"v6\",\"v7\",\"v8\",\"v9\",\"v10\",\"v11\",\"v12\"]}"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-var_body_idx12 == "v12"
+ expect resp.http.x-var_body_idx13 == ""
+
+ txreq -url "/" \
+ -body "{\"integer\":1e}"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-var_body_bad_int == ""
+
+ txreq -url "/" \
+ -body "{\"newNonce\":\"https:\\/\\/acme-v02.api.letsencrypt.org\\/acme\\/new-nonce\",\"newAccount\":\"https://acme-v02.api.letsencrypt.org/acme/new-acct\",\"newOrder\":\"https://acme-v02.api.letsencrypt.org/acme/new-order\"}"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-var_acme_nonce == "https://acme-v02.api.letsencrypt.org/acme/new-nonce"
+
+ txreq -url "/" \
+ -body "{\"authorizations\":[\"a0\",\"a1\",\"a2\",\"a3\",\"a4\",\"a5\",\"a6\",\"a7\",\"a8\",\"a9\",\"a10\",\"a11\",\"https://ca.example.test/authz/12\"],\"finalize\":\"https://ca.example.test/finalize/42\"}"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-var_acme_order12 == "https://ca.example.test/authz/12"
+
+ txreq -url "/" \
+ -body "{\"error\":{\"type\":\"urn:ietf:params:acme:error:accountDoesNotExist\",\"detail\":\"missing account\"}}"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.x-var_acme_err_type == "urn:ietf:params:acme:error:accountDoesNotExist"
+
} -run
--
2.43.0
From 289c033d0c98febc7c6d8a5ce521f415150ba581 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 13/14] TEST/MEDIUM: tests: cover escaped JWK strings in
jwt_decrypt
Exercise JWK parsing with valid escaped JSON strings.
These cases cover escaped forward slashes and unicode escapes in string
fields that are consumed by the JWK/JOSE parsing path, to make sure the
stricter mjson handling remains compatible with valid JWT inputs.
---
reg-tests/jwt/jwt_decrypt.vtc | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/reg-tests/jwt/jwt_decrypt.vtc b/reg-tests/jwt/jwt_decrypt.vtc
index 7a0c2a2645..fbd482f456 100644
--- a/reg-tests/jwt/jwt_decrypt.vtc
+++ b/reg-tests/jwt/jwt_decrypt.vtc
@@ -272,6 +272,20 @@ client c8 -connect ${h1_mainfe_sock} {
} -run
+# Test 'jwt_decrypt_jwk' with valid JSON string escapes in the JWK
+client c8_1 -connect ${h1_mainfe_sock} {
+ txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiAiZGlyIiwgImVuYyI6ICJBMjU2R0NNIn0..hxCk0nP4aVNpgfb7.inlyAZtUzDCTpD_9iuWx.Pyu90cmgkXenMIVu9RUp8w" \
+ -hdr "X-JWK: {\"kty\":\"\\u006fct\", \"kid\":\"https:\\/\\/issuer.example.test\\/keys\\/1\", \"k\":\"ZMpktzGq1g6_r4fKVdnx9OaYr4HjxPjIs7l7SwAsgsg\"}"
+ rxresp
+ expect resp.http.x-decrypted == "Setec Astronomy"
+
+ txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiAiUlNBMV81IiwgImVuYyI6ICJBMjU2R0NNIn0.ew8AbprGcd_J73-CZPIsE1YonD9rtcL7VCuOOuVkrpS_9UzA9_kMh1yw20u-b5rKJAhmFMCQPXl44ro6IzOeHu8E2X_NlPEnQfyNVQ4R1HB_E9sSk5BLxOH3aHkVUh0I-e2eDDj-pdI3OrdjZtnZEBeQ7tpMcoBEbn1VGg7Pmw4qtdS-0qnDSs-PttU-cejjgPUNLRU8UdoRVC9uJKacJms110QugDuFuMYTTSU2nbIYh0deCMRAuKGWt0Ii6EMYW2JaJ7JfXag59Ar1uylQPyEVrocnOsDuB9xnp2jd796qCPdKxBK9yKUnwjal4SQpYbutr40QzG1S4MsKaUorLg.0el2ruY0mm2s7LUR.X5RI6dF06Y_dbAr8meb-6SG5enj5noto9nzgQU5HDrYdiUofPptIf6E-FikKUM9QR4pY9SyphqbPYeAN1ZYVxBrR8tUf4Do2kw1biuuRAmuIyytpmxwvY946T3ctu1Zw3Ymwe-jWXX08EngzssvzFOGT66gkdufrTkC45Fkr0RBOmWa5OVVg_VR6LwcivtQMmlArlrwbaDmmLqt_2p7afT0UksEz4loq0sskw-p7GbhB2lpzXoDnijdHrQkftRbVCiDbK4-qGr7IRFb0YOHvyVFr-kmDoJv2Zsg_rPKV1LkYmPJUbVDo9T3RAcLinlKPK4ZPC_2bWj3M9BvfOq1HeuyVWzX2Cb1mHFdxXFGqaLPfsE0VOfn0GqL7oHVbuczYYw2eKdmiw5LEMwuuJEdYDE9IIFEe8oRB4hNZ0XMYB6oqqZejD0Fh6nqlj5QUrTYpTSE-3LkgK2zRJ0oZFXZyHCB426bmViuE0mXF7twkQep09g0U35-jFBZcSYBDvZZL1t5d_YEQ0QtO0mEeEpGb0Pvk_EsSMFib7NxClz4_rdtwWCFuM4uFOS5vrQMiMqi_TadhLxrugRFhJpsibuScCiJ7eNDrUvwSWEwv1U593MUX3guDq_ONOo_49EOJSyRJtQCNC6FW6GLWSz9TCo6g5LCnXt-pqwu0Iymr7ZTQ3MTsdq2G55JM2e6SdG43iET8r235hynmXHKPUYHlSjsC2AEAY_pGDO0akIhf4wDVIM5rytn-rjQf-29ZJp05g6KPe-EaN1C-X7aBGhgAEgnX-iaXXbotpGeKRTNj2jAG1UrkYi6BGHxluiXJ8jH_LjHuxKyzIObqK8p28ePDKRL-jyNTrvGW2uorgb_u7HGmWYIWLTI7obnZ5vw3MbkjcwEd4bX5JXUj2rRsUWMlZSSFVO9Wgf7MBvcLsyF0Yqun3p0bi__edmcqNF_uuYZT-8jkUlMborqIDDCYYqIolgi5R1Bmut-gFYq6xyfEncxOi50xmYon50UulVnAH-up_RELGtCjmAivaJb8.upVY733IMAT8YbMab2PZnw" \
+ -hdr "X-JWK: {\"kty\":\"R\\u0053A\", \"e\":\"AQAB\", \"n\":\"wsqJbopx18NQFYLYOq4ZeMSE89yGiEankUpf25yV8QqroKUGrASj_OeqTWUjwPGKTN1vGFFuHYxiJeAUQH2qQPmg9Oqk6-ATBEKn9COKYniQ5459UxCwmZA2RL6ufhrNyq0JF3GfXkjLDBfhU9zJJEOhknsA0L_c-X4AI3d_NbFdMqxNe1V_UWAlLcbKdwO6iC9fAvwUmDQxgy6R0DC1CMouQpenMRcALaSHar1cm4K-syoNobv3HEuqgZ3s6-hOOSqauqAO0GUozPpaIA7OeruyRl5sTWT0r-iz39bchID2bIKtcqLiFcSYPLBcxmsaQCqRlGhmv6stjTCLV1yT9w\", \"kid\":\"ff3c5c96-392e-46ef-a839-6ff16027af78\", \"d\":\"b9hXfQ8lOtw8mX1dpqPcoElGhbczz_-xq2znCXQpbBPSZBUddZvchRSH5pSSKPEHlgb3CSGIdpLqsBCv0C_XmCM9ViN8uqsYgDO9uCLIDK5plWttbkqA_EufvW03R9UgIKWmOL3W4g4t-C2mBb8aByaGGVNjLnlb6i186uBsPGkvaeLHbQcRQKAvhOUTeNiyiiCbUGJwCm4avMiZrsz1r81Y1Z5izo0ERxdZymxM3FRZ9vjTB-6DtitvTXXnaAm1JTu6TIpj38u2mnNLkGMbflOpgelMNKBZVxSmfobIbFN8CHVc1UqLK2ElsZ9RCQANgkMHlMkOMj-XT0wHa3VBUQ\", \"p\":\"8mgriveKJAp1S7SHqirQAfZafxVuAK_A2QBYPsAUhikfBOvN0HtZjgurPXSJSdgR8KbWV7ZjdJM_eOivIb_XiuAaUdIOXbLRet7t9a_NJtmX9iybhoa9VOJFMBq_rbnbbte2kq0-FnXmv3cukbC2LaEw3aEcDgyURLCgWFqt7M0\", \"q\":\"zbbTv5421GowOfKVEuVoA35CEWgl8mdasnEZac2LWxMwKExikKU5LLacLQlcOt7A6n1ZGUC2wyH8mstO5tV34Eug3fnNrbnxFUEE_ZB_njs_rtZnwz57AoUXOXVnd194seIZF9PjdzZcuwXwXbrZ2RSVW8if_ZH5OVYEM1EsA9M\", \"dp\":\"1BaIYmIKn1X3InGlcSFcNRtSOnaJdFhRpotCqkRssKUx2qBlxs7ln_5dqLtZkx5VM_UE_GE7yzc6BZOwBxtOftdsr8HVh-14ksSR9rAGEsO2zVBiEuW4qZf_aQM-ScWfU--wcczZ0dT-Ou8P87Bk9K9fjcn0PeaLoz3WTPepzNE\", \"dq\":\"kYw2u4_UmWvcXVOeV_VKJ5aQZkJ6_sxTpodRBMPyQmkMHKcW4eKU1mcJju_deqWadw5jGPPpm5yTXm5UkAwfOeookoWpGa7CvVf4kPNI6Aphn3GBjunJHNpPuU6w-wvomGsxd-NqQDGNYKHuFFMcyXO_zWXglQdP_1o1tJ1M-BM\", \"qi\":\"j94Ens784M8zsfwWoJhYq9prcSZOGgNbtFWQZO8HP8pcNM9ls7YA4snTtAS_B4peWWFAFZ0LSKPCxAvJnrq69ocmEKEk7ss1Jo062f9pLTQ6cnhMjev3IqLocIFt5Vbsg_PWYpFSR7re6FRbF9EYOM7F2-HRv1idxKCWoyQfBqk\"}"
+ rxresp
+ expect resp.http.x-decrypted == "Sed ut perspiciatis unde omnis iste natus error sit voluptatem doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. porro quisquam est, qui dolorem ipsum quia dolor sit amet, adipisci velit, sed quia non numquam eius modi tempora incidunt ut dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in voluptate velit esse quam nihil molestiae consequatur, vel illum qui eum fugiat quo voluptas nulla pariatur?"
+} -run
+
+
# ECDH-ES
client c9 -connect ${h1_mainfe_sock} {
txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiAiRUNESC1FUyIsICJlbmMiOiAiQTI1NkdDTSIsICJlcGsiOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJZVUg1VlJweURCb3FiUjVCUWlSMGV4anJ0bDdLb24yeTZkejFnM3NuNzZJIiwgInkiOiAiZld2RHFHb3pYNjJnMnRTS19oSkctWkVKNkFCcFhYTS1Tc1hPeE5KUXFtUSJ9fQ..0tN70AQ3P_4uEV4t.zkv7KfnUlDTKjJ82zKCMK_z7OEFk_euXGuJemShf8mnOeEUE4UN8wS5cRJzMQWxcY9d3dIvUCYx0HhzeoXnKqnkEU6be659IVtKpqtceLYKcIkpjj0XiaEalVqIKKXTU2NG2ldNsYwnEDN_XxMnIUPFOy3yJqpOfjf8v98ABYuTWfJVwk3tK9vYCj-ScCf2NK7cEIti_09VCsxMg7z0kvco5UaTXvDjEbPhj_EVfHoPlmDE6EuaO5OX5t3reOoJ1vsM2PEpADiYfmvSZxeWAmmtAH7cvrRIUCcy4Q5pNczh1Pmt0y-uJKtme16YWq8PxVtnb7lY9HDTuPeaMVqvMV6PlQ9vnfsirjpz72qx3ArAeXkIGJsPOGKfgCoW6sAWHQxCzvq8ek7zOaqTAo169PSdtxfBL4MJWxoLg38pODy4cjEGR71YYirthejEMgRs7G1A8ksxgs2bkYGInunUD_iAWkQzxYZhFlLRntWP1ikOKmx9gbqR6K9UiqCK1UG4NXF3o4OV34m-jw-cXMDF2JkekVK2-rhxTbXmqP-VhDrkQ2ANdk7fTW9elFYNisVzE1QjdClMKGhO1fdKiSJ9xSPo3W6pMuquYYN-XT1fLiu3GDtO4ELZWVdwmiucsxv9H2jzPwbhvbvlXwXsmyCBtvumcEUbiYCOIYvlddhTGjZHplvDU73O5SkxUYJTYh7H0DcSiZ-6tcWdRCs605xVZMJ_X91_gZ1tb2_df73lYT_tVo39kw78m3GVFBeK2Zy4JeLheo0fHE7n8lg13uwG77SHwrWSV61KKWhBPZR0bWGi8YvVHnqX0GWklIjpqjbIjYAk4baFv4MO4OvEkPxnGm64NNZWrGEA0U8eEHCgjF1ZagQFNb674Crgd-tRA0QPEAOc9NsnlK1Q-47KIgqNbwoc3VpbpHNLVJT4aKWV5q187YNxarbpeDqguh75M9AgbpT5bSDFhjF83f1kiEDgLdNTkAd-CPAzgtzaEAfxD1K4ViZZZ2DqXgw0PFTFZAWrWqv8Ydi61r5MJ.Srleju8Bifrc_6bqFPUF_w" \
--
2.43.0
From e77e62cf9ae262a83c3ad083f18c7be6a2219833 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 12/14] BUG/MEDIUM: mjson: validate hex input before decoding
Reject malformed hexadecimal input before decoding it.
The previous implementation would silently map non-hex characters to byte
values and also accepted odd-length input. Failing fast here makes invalid
JSON data easier to diagnose and avoids propagating corrupted decoded output.
---
src/mjson.c | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index 8f8a621476..a5c075f916 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -385,7 +385,7 @@ int mjson_get_bool(const char *s, int len, const char *path, int *v) {
return tok == MJSON_TOK_TRUE || tok == MJSON_TOK_FALSE ? 1 : 0;
}
-static int __attribute__((unused)) mjson_unhex_byte(const char *s) {
+static int mjson_unhex_byte(const char *s) {
int hi = mjson_hexval(s[0]), lo = mjson_hexval(s[1]);
if (hi < 0 || lo < 0) return -1;
return (hi << 4) | lo;
@@ -399,7 +399,11 @@ static int mjson_unescape(const char *s, int len, char *to, int n) {
// \u00xx from the ASCII range. More complex chars would require
// dragging in a UTF8 library, which is too much for us
if (s[i + 2] != '0' || s[i + 3] != '0') return -1; // Too much, give up
- to[j] = mjson_unhex_nimble(s + i + 4);
+ {
+ int ch = mjson_unhex_byte(s + i + 4);
+ if (ch < 0) return -1;
+ to[j] = ch;
+ }
i += 5;
} else if (s[i] == '\\' && i + 1 < len) {
int c = s[i + 1] == '/' ? '/' : mjson_esc(s[i + 1], 0);
@@ -427,9 +431,13 @@ int mjson_get_hex(const char *s, int len, const char *x, char *to, int n) {
const char *p;
int i, j, sz;
if (mjson_find(s, len, x, &p, &sz) != MJSON_TOK_STRING) return -1;
+ if (((sz - 2) & 1) != 0) return -1;
for (i = j = 0; i < sz - 3 && j < n; i += 2, j++) {
- ((unsigned char *) to)[j] = mjson_unhex_nimble(p + i + 1);
+ int ch = mjson_unhex_byte(p + i + 1);
+ if (ch < 0) return -1;
+ ((unsigned char *) to)[j] = ch;
}
+ if (i < sz - 2) return -1;
if (j < n) to[j] = '\0';
return j;
}
--
2.43.0
From eb0c792b6f9c53f991f062b51280691dad9e5a32 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 11/14] BUG/LOW: mjson: add validated hex byte decoder
Add a helper that decodes one byte from two validated hex digits.
This is a small refactoring step that prepares the stricter handling of
unicode and raw hex decoding in later commits.
---
src/mjson.c | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index b77e94632d..8f8a621476 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -385,15 +385,10 @@ int mjson_get_bool(const char *s, int len, const char *path, int *v) {
return tok == MJSON_TOK_TRUE || tok == MJSON_TOK_FALSE ? 1 : 0;
}
-static unsigned char mjson_unhex_nimble(const char *s) {
- unsigned char i, v = 0;
- for (i = 0; i < 2; i++) {
- int c = s[i];
- if (i > 0) v <<= 4;
- v |= (c >= '0' && c <= '9') ? c - '0'
- : (c >= 'A' && c <= 'F') ? c - '7' : c - 'W';
- }
- return v;
+static int __attribute__((unused)) mjson_unhex_byte(const char *s) {
+ int hi = mjson_hexval(s[0]), lo = mjson_hexval(s[1]);
+ if (hi < 0 || lo < 0) return -1;
+ return (hi << 4) | lo;
}
static int mjson_unescape(const char *s, int len, char *to, int n) {
--
2.43.0
From 2d345769ffe00f2b1d55b69a555aea08970672ae Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 10/14] BUG/HIGH: mjson: validate paths and array indexes
Validate JSON paths and parse array indexes defensively.
The previous code assumed well-formed bracket syntax and could read past the
end of the path string while looking for a closing bracket. Hardening this
logic avoids undefined behaviour and reduces the risk from malformed,
attacker-controlled paths.
---
src/mjson.c | 20 +++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index b552d9296f..b77e94632d 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -113,9 +113,7 @@ static int mjson_pass_number(const char *s, int len) {
return i;
}
-static int __attribute__((unused)) mjson_parse_array_index(const char *s,
- int *index,
- int *consumed) {
+static int mjson_parse_array_index(const char *s, int *index, int *consumed) {
int i = 1, value = 0;
if (s[0] != '[' || !is_digit(s[i])) return -1;
@@ -131,7 +129,7 @@ static int __attribute__((unused)) mjson_parse_array_index(const char *s,
return 0;
}
-static int __attribute__((unused)) mjson_is_valid_path(const char *path) {
+static int mjson_is_valid_path(const char *path) {
int pos = 0;
if (path[pos++] != '$') return 0;
@@ -310,11 +308,13 @@ static int mjson_get_cb(int tok, const char *s, int off, int len, void *ud) {
data->d1++;
} else if (tok == '[') {
if (data->d1 == data->d2 && data->path[data->pos] == '[') {
+ int consumed = 0;
data->i1 = 0;
- data->i2 = (int) mystrtod(&data->path[data->pos + 1], NULL);
+ if (mjson_parse_array_index(&data->path[data->pos], &data->i2, &consumed) < 0)
+ return 1;
if (data->i1 == data->i2) {
data->d2++;
- data->pos += 3;
+ data->pos += consumed;
}
}
if (!data->path[data->pos] && data->d1 == data->d2) data->obj = off;
@@ -323,8 +323,10 @@ static int mjson_get_cb(int tok, const char *s, int off, int len, void *ud) {
if (data->d1 == data->d2 + 1) {
data->i1++;
if (data->i1 == data->i2) {
- while (data->path[data->pos] != ']') data->pos++;
- data->pos++;
+ int consumed = 0;
+ if (mjson_parse_array_index(&data->path[data->pos], NULL, &consumed) < 0)
+ return 1;
+ data->pos += consumed;
data->d2++;
}
}
@@ -362,7 +364,7 @@ enum mjson_tok mjson_find(const char *s, int len, const char *jp,
const char **tokptr, int *toklen) {
struct mjson_get_data data = {jp, 1, 0, 0, 0,
0, -1, tokptr, toklen, MJSON_TOK_INVALID};
- if (jp[0] != '$') return MJSON_TOK_INVALID;
+ if (!mjson_is_valid_path(jp)) return MJSON_TOK_INVALID;
if (mjson(s, len, mjson_get_cb, &data) < 0) return MJSON_TOK_INVALID;
return (enum mjson_tok) data.tok;
}
--
2.43.0
From 7dd9d8a9fb5b1660cf663121158d831b69a11223 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 09/14] BUG/LOW: mjson: add path validation helpers
Add helpers for validating JSON paths and parsing array indexes.
This isolates the path syntax checks from the traversal logic and sets
up the following hardening of mjson_find path handling.
---
src/mjson.c | 43 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/src/mjson.c b/src/mjson.c
index 7d2a7b465b..b552d9296f 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -19,13 +19,14 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-#include <float.h>
+#include <limits.h>
#include <string.h>
#include <import/mjson.h>
static double mystrtod(const char *str, char **end);
static int is_digit(int c);
+static int plen2(const char *s);
static int mjson_hexval(int c) {
if (c >= '0' && c <= '9') return c - '0';
@@ -112,6 +113,46 @@ static int mjson_pass_number(const char *s, int len) {
return i;
}
+static int __attribute__((unused)) mjson_parse_array_index(const char *s,
+ int *index,
+ int *consumed) {
+ int i = 1, value = 0;
+
+ if (s[0] != '[' || !is_digit(s[i])) return -1;
+ do {
+ if (value > (INT_MAX - (s[i] - '0')) / 10) return -1;
+ value = value * 10 + (s[i] - '0');
+ i++;
+ } while (is_digit(s[i]));
+
+ if (s[i] != ']') return -1;
+ if (index != NULL) *index = value;
+ if (consumed != NULL) *consumed = i + 1;
+ return 0;
+}
+
+static int __attribute__((unused)) mjson_is_valid_path(const char *path) {
+ int pos = 0;
+
+ if (path[pos++] != '$') return 0;
+
+ while (path[pos] != '\0') {
+ if (path[pos] == '.') {
+ int len = plen2(&path[pos + 1]);
+ if (len <= 0) return 0;
+ pos += len + 1;
+ } else if (path[pos] == '[') {
+ int consumed = 0;
+ if (mjson_parse_array_index(&path[pos], NULL, &consumed) < 0) return 0;
+ pos += consumed;
+ } else {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
int mjson(const char *s, int len, mjson_cb_t cb, void *ud) {
enum { S_VALUE, S_KEY, S_COLON, S_COMMA_OR_EOO } expecting = S_VALUE;
unsigned char nesting[MJSON_MAX_DEPTH];
--
2.43.0
From 664f25f33b2a70e4d732eb53da41c14fc65c6b99 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 08/14] BUG/HIGH: mjson: validate number tokens before parsing
Reject invalid JSON numbers before tokenizing them.
Without this check, malformed input such as an incomplete exponent may be
accepted as a number token and can cause the parser to stop making progress.
If attacker-controlled JSON reaches this code path, this can turn into a
denial-of-service risk for HAProxy.
---
src/mjson.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index 4467282f21..7d2a7b465b 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -46,7 +46,7 @@ static int mjson_escape(int c) {
return mjson_esc(c, 1);
}
-static int __attribute__((unused)) mjson_is_delim(int c) {
+static int mjson_is_delim(int c) {
return c == ',' || c == ']' || c == '}' || c == ' ' || c == '\t' ||
c == '\n' || c == '\r';
}
@@ -77,7 +77,7 @@ static int mjson_pass_string(const char *s, int len) {
return MJSON_ERROR_INVALID_INPUT;
}
-static int __attribute__((unused)) mjson_pass_number(const char *s, int len) {
+static int mjson_pass_number(const char *s, int len) {
int i = 0;
if (len <= 0) return MJSON_ERROR_INVALID_INPUT;
@@ -160,9 +160,9 @@ int mjson(const char *s, int len, mjson_cb_t cb, void *ud) {
i += 4;
tok = MJSON_TOK_FALSE;
} else if (c == '-' || ((c >= '0' && c <= '9'))) {
- char *end = NULL;
- mystrtod(&s[i], &end);
- if (end != NULL) i += (int) (end - &s[i] - 1);
+ int n = mjson_pass_number(&s[i], len - i);
+ if (n < 0) return n;
+ i += n - 1;
tok = MJSON_TOK_NUMBER;
} else if (c == '"') {
int n = mjson_pass_string(&s[i + 1], len - i - 1);
--
2.43.0
From 3941a66a2f08187c54257fa0526cf90ae510f601 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 07/14] BUG/LOW: mjson: add number validation helpers
Add helpers for checking JSON number syntax before tokenization.
These helpers implement the shape of valid JSON numbers and are wired
into the parser by the following commit.
---
src/mjson.c | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/mjson.c b/src/mjson.c
index bf641fb5d5..4467282f21 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -25,6 +25,7 @@
#include <import/mjson.h>
static double mystrtod(const char *str, char **end);
+static int is_digit(int c);
static int mjson_hexval(int c) {
if (c >= '0' && c <= '9') return c - '0';
@@ -45,6 +46,11 @@ static int mjson_escape(int c) {
return mjson_esc(c, 1);
}
+static int __attribute__((unused)) mjson_is_delim(int c) {
+ return c == ',' || c == ']' || c == '}' || c == ' ' || c == '\t' ||
+ c == '\n' || c == '\r';
+}
+
static int mjson_pass_string(const char *s, int len) {
int i;
for (i = 0; i < len; i++) {
@@ -71,6 +77,41 @@ static int mjson_pass_string(const char *s, int len) {
return MJSON_ERROR_INVALID_INPUT;
}
+static int __attribute__((unused)) mjson_pass_number(const char *s, int len) {
+ int i = 0;
+
+ if (len <= 0) return MJSON_ERROR_INVALID_INPUT;
+ if (s[i] == '-') {
+ i++;
+ if (i >= len) return MJSON_ERROR_INVALID_INPUT;
+ }
+
+ if (s[i] == '0') {
+ i++;
+ } else if (is_digit(s[i])) {
+ while (i < len && is_digit(s[i])) i++;
+ } else {
+ return MJSON_ERROR_INVALID_INPUT;
+ }
+
+ if (i < len && s[i] == '.') {
+ i++;
+ if (i >= len || !is_digit(s[i])) return MJSON_ERROR_INVALID_INPUT;
+ while (i < len && is_digit(s[i])) i++;
+ }
+
+ if (i < len && (s[i] == 'e' || s[i] == 'E')) {
+ i++;
+ if (i < len && (s[i] == '+' || s[i] == '-')) i++;
+ if (i >= len || !is_digit(s[i])) return MJSON_ERROR_INVALID_INPUT;
+ while (i < len && is_digit(s[i])) i++;
+ }
+
+ if (i < len && !mjson_is_delim(s[i])) return MJSON_ERROR_INVALID_INPUT;
+
+ return i;
+}
+
int mjson(const char *s, int len, mjson_cb_t cb, void *ud) {
enum { S_VALUE, S_KEY, S_COLON, S_COMMA_OR_EOO } expecting = S_VALUE;
unsigned char nesting[MJSON_MAX_DEPTH];
--
2.43.0
From ae87c91fda4b163773a5664de5dcd52ce1ecffc9 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 06/14] BUG/LOW: mjson: validate unicode escape digits
Validate the hexadecimal digits used by unicode escape sequences.
This ensures that malformed \uXXXX escapes are rejected instead of
being partially accepted by the string parser.
---
src/mjson.c | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index 0ca4b1e34b..bf641fb5d5 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -26,7 +26,7 @@
static double mystrtod(const char *str, char **end);
-static int __attribute__((unused)) mjson_hexval(int c) {
+static int mjson_hexval(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
@@ -50,9 +50,20 @@ static int mjson_pass_string(const char *s, int len) {
for (i = 0; i < len; i++) {
if (s[i] == '"') {
return i;
- } else if (s[i] == '\\' && i + 1 < len &&
- (mjson_escape(s[i + 1]) || s[i + 1] == '/')) {
- i++;
+ } else if (s[i] == '\\') {
+ if (i + 1 >= len) return MJSON_ERROR_INVALID_INPUT;
+ if (mjson_escape(s[i + 1]) || s[i + 1] == '/') {
+ i++;
+ } else if (s[i + 1] == 'u') {
+ int j;
+ if (i + 5 >= len) return MJSON_ERROR_INVALID_INPUT;
+ for (j = 0; j < 4; j++) {
+ if (mjson_hexval(s[i + 2 + j]) < 0) return MJSON_ERROR_INVALID_INPUT;
+ }
+ i += 5;
+ } else {
+ return MJSON_ERROR_INVALID_INPUT;
+ }
} else if (s[i] == '\0' || ((unsigned char) s[i]) < 0x20) {
return MJSON_ERROR_INVALID_INPUT;
}
--
2.43.0
From db6f047050c95539423e936b4e20374cf90c68a1 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 05/14] BUG/LOW: mjson: add hex digit helper
Introduce a helper that validates and converts a single hex digit.
This is a small preparatory step used by the later unicode and hex
decoding hardening changes.
---
src/mjson.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/mjson.c b/src/mjson.c
index ffbba3d597..0ca4b1e34b 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -26,6 +26,13 @@
static double mystrtod(const char *str, char **end);
+static int __attribute__((unused)) mjson_hexval(int c) {
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
static int mjson_esc(int c, int esc) {
const char *p, *esc1 = "\b\f\n\r\t\\\"", *esc2 = "bfnrt\\\"";
for (p = esc ? esc1 : esc2; *p != '\0'; p++) {
--
2.43.0
From d275b365b326332c5bb52702af38b0198e008cb7 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 04/14] BUG/LOW: mjson: tighten basic string escape handling
Reject invalid control characters and accept escaped forward slashes.
This moves the string parser closer to JSON rules and prepares the
subsequent hardening of unicode escape handling.
---
src/mjson.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index 7adbd58adf..ffbba3d597 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -41,12 +41,13 @@ static int mjson_escape(int c) {
static int mjson_pass_string(const char *s, int len) {
int i;
for (i = 0; i < len; i++) {
- if (s[i] == '\\' && i + 1 < len && mjson_escape(s[i + 1])) {
+ if (s[i] == '"') {
+ return i;
+ } else if (s[i] == '\\' && i + 1 < len &&
+ (mjson_escape(s[i + 1]) || s[i + 1] == '/')) {
i++;
- } else if (s[i] == '\0') {
+ } else if (s[i] == '\0' || ((unsigned char) s[i]) < 0x20) {
return MJSON_ERROR_INVALID_INPUT;
- } else if (s[i] == '"') {
- return i;
}
}
return MJSON_ERROR_INVALID_INPUT;
@@ -304,7 +305,7 @@ static int mjson_unescape(const char *s, int len, char *to, int n) {
to[j] = mjson_unhex_nimble(s + i + 4);
i += 5;
} else if (s[i] == '\\' && i + 1 < len) {
- int c = mjson_esc(s[i + 1], 0);
+ int c = s[i + 1] == '/' ? '/' : mjson_esc(s[i + 1], 0);
if (c == 0) return -1;
to[j] = c;
i++;
--
2.43.0
From 7c783f54afe09d92573d45c496871967cafba0e7 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 03/14] CLEANUP/LOW: mjson: remove unused mystrtod counter
Drop an unused local counter from mystrtod.
This keeps the helper free of dead code and removes the need for a
special unused annotation on the variable.
---
src/mjson.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index e19628d314..7adbd58adf 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -343,7 +343,7 @@ static int is_digit(int c) {
/* NOTE: strtod() implementation by Yasuhiro Matsumoto. */
static double mystrtod(const char *str, char **end) {
double d = 0.0;
- int sign = 1, __attribute__((unused)) n = 0;
+ int sign = 1;
const char *p = str, *a = str;
/* decimal part */
@@ -358,7 +358,6 @@ static double mystrtod(const char *str, char **end) {
while (*p && is_digit(*p)) {
d = d * 10.0 + (double) (*p - '0');
++p;
- ++n;
}
a = p;
} else if (*p != '.') {
@@ -377,7 +376,6 @@ static double mystrtod(const char *str, char **end) {
f += base * (*p - '0');
base /= 10.0;
++p;
- ++n;
}
}
d += f * sign;
--
2.43.0
From 2c44393d10b7797f02bba00c75ef4638186907c9 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 02/14] CLEANUP/LOW: mjson: drop unused stdio include
Remove an unused standard I/O include from mjson.
The file does not use stdio interfaces, so dropping the include keeps
the dependencies minimal and avoids unnecessary clutter.
---
src/mjson.c | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index 2949fe5de1..e19628d314 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -171,8 +171,6 @@ struct mjson_get_data {
int tok; // Returned token
};
-#include <stdio.h>
-
static int plen1(const char *s) {
int i = 0, n = 0;
while (s[i] != '\0' && s[i] != '.' && s[i] != '[')
@@ -428,4 +426,3 @@ static double mystrtod(const char *str, char **end) {
if (end) *end = (char *) a;
return d;
}
-
--
2.43.0
From c9038457a2d0c292d2cf4a77f650e319298be8a1 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Sun, 15 Mar 2026 20:37:33 +0100
Subject: [PATCH 01/14] CLEANUP/LOW: mjson: rename mjson_get_data state struct
Rename the internal mjson path lookup state structure.
This is a pure naming cleanup that fixes a typo in the struct name and
makes the code easier to read without changing behaviour.
---
src/mjson.c | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/mjson.c b/src/mjson.c
index a36fe185f9..2949fe5de1 100644
--- a/src/mjson.c
+++ b/src/mjson.c
@@ -158,7 +158,7 @@ int mjson(const char *s, int len, mjson_cb_t cb, void *ud) {
return MJSON_ERROR_INVALID_INPUT;
}
-struct msjon_get_data {
+struct mjson_get_data {
const char *path; // Lookup json path
int pos; // Current path index
int d1; // Current depth of traversal
@@ -200,7 +200,7 @@ static int kcmp(const char *a, const char *b, int n) {
}
static int mjson_get_cb(int tok, const char *s, int off, int len, void *ud) {
- struct msjon_get_data *data = (struct msjon_get_data *) ud;
+ struct mjson_get_data *data = (struct mjson_get_data *) ud;
// printf("--> %2x %2d %2d %2d %2d\t'%s'\t'%.*s'\t\t'%.*s'\n", tok, data->d1,
// data->d2, data->i1, data->i2, data->path + data->pos, off, s, len,
// s + off);
@@ -261,7 +261,7 @@ static int mjson_get_cb(int tok, const char *s, int off, int len, void *ud) {
enum mjson_tok mjson_find(const char *s, int len, const char *jp,
const char **tokptr, int *toklen) {
- struct msjon_get_data data = {jp, 1, 0, 0, 0,
+ struct mjson_get_data data = {jp, 1, 0, 0, 0,
0, -1, tokptr, toklen, MJSON_TOK_INVALID};
if (jp[0] != '$') return MJSON_TOK_INVALID;
if (mjson(s, len, mjson_get_cb, &data) < 0) return MJSON_TOK_INVALID;
@@ -429,4 +429,3 @@ static double mystrtod(const char *str, char **end) {
return d;
}
-
--
2.43.0