Thank you Aleksandar, I already started to look at the current mjson code to fix the current issues, I'll mayve consider some of these patches if appropriate.
On Mon, Mar 16, 2026 at 11:09:21AM +0100, Aleksandar Lazic wrote: > Subject: Some mjson cleanups ~14 patches > 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_rdtwWCFuM4uF> > > OS5vrQMiMqi_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\":\"zbbTv5421G> > owOfKVEuVoA35CEWgl8mdasnEZac2LWxMwKExikKU5LLacLQlcOt7A6n1ZGUC2wyH8mstO5tV34Eug3fnNrbnxFUEE_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-6tcWd> > > RCs605xVZMJ_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 > -- William Lallemand

