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



Reply via email to