Skip to content

jose: Fix NULL pointer dereference and OOB read in compact JWE decoding - #74

Open
kunwaratulhax0r wants to merge 1 commit into
apache:trunkfrom
kunwaratulhax0r:fix/jwe-compact-decode-nullderef
Open

kunwaratulhax0r wants to merge 1 commit into
apache:trunkfrom
kunwaratulhax0r:fix/jwe-compact-decode-nullderef

Conversation

@kunwaratulhax0r

Copy link
Copy Markdown

Summary

Two bugs in Compact Serialization JWE decoding, both in jose/apr_jose_decode.c / jose/apr_jose.c:

1. NULL pointer dereference in apr_jose_jwe_make() (jose/apr_jose.c)

jose->cty = payload->cty;

payload is dereferenced unconditionally, but it's documented and declared as optional — the function's own __attribute__((nonnull(6))) covers only the pool parameter, deliberately excluding payload. apr_jose_decode_compact_jwe() always calls this with payload == NULL (no payload object exists yet at that point in decryption), so every call to apr_jose_decode() on a JWE in Compact Serialization crashes, for any well-formed input, not just malformed input.

The sibling constructors apr_jose_jws_make, apr_jose_jwe_json_make, and apr_jose_jws_json_make all correctly guard the identical line with if (payload) { ... }. apr_jose_jwe_make is the only one of the four missing it.

2. Out-of-bounds read in apr_jose_decode_compact_jwe() (jose/apr_jose_decode.c)

left = dot + 1;

/* tag */
jwe->encryption->tag.data = apr_pdecode_base64_binary(pool, left,
        dot - left, APR_ENCODE_BASE64URL, &jwe->encryption->tag.len);

dot is the delimiter found for the previous (ciphertext) segment, and left was just set to dot + 1 on the line above — so dot - left is always exactly -1. Passed as the signed slen argument to apr_pdecode_base64_binary(), -1 is APR_ENCODE_STRING, which tells the decoder to scan for a NUL terminator instead of respecting a length, reading past the end of the real input buffer. The tag is the last segment in Compact Serialization (no trailing delimiter), so its length should be measured against the end of the input, right, not against the previous segment's delimiter.

This second bug was masked by the first — the NULL deref happens earlier in the same call path, so the OOB read was never actually reached until bug 1 was fixed.

Verification

Built apache/apr trunk from source and reproduced both issues with a standalone test calling apr_jose_decode() with a real decrypt callback on a well-formed Compact JWE string:

  • Before any fix: crashes (SIGSEGV) inside apr_jose_jwe_make, called from apr_jose_decode_compact_jwe, before the decrypt callback is ever reached.
  • After fixing bug 1 only: no crash, decrypt callback is reached, but reports a tag length that reads past the intended segment into adjacent memory (confirmed by placing known marker bytes after the legitimate buffer).
  • After fixing both: no crash, and the reported tag length exactly matches the actual encoded tag, with no overread.

Scope note

Neither released APR (≤ 1.7.6) nor apr-util (≤ 1.6.5) ships the jose module — it exists only on unreleased development branches (APR 2.0.0-dev on trunk, and an apr-util 1.7.x branch that has never had a tagged release). This is a pre-release bug fix, not a report against any shipped version.

Test plan

  • Confirmed crash reproduces on trunk before this change
  • Confirmed no crash after this change
  • Confirmed the OOB read (wrong tag length reading adjacent memory) before the second fix
  • Confirmed correct tag length after the second fix
  • Existing apr_jose test suite (if any) — please let me know if there's a specific test target to run

apr_jose_jwe_make() dereferenced its payload argument unconditionally
(jose->cty = payload->cty) even though payload is documented and
declared (via __attribute__((nonnull(6))), which excludes the payload
parameter) as optional. apr_jose_decode_compact_jwe() always calls it
with payload == NULL, since no payload exists yet at that point in
decryption. This means every call to apr_jose_decode() on a JWE in
Compact Serialization crashes with a NULL pointer dereference, for
any well-formed input, not just malformed input.

The sibling constructors (apr_jose_jws_make, apr_jose_jwe_json_make,
apr_jose_jws_json_make) all correctly guard the same line with
`if (payload)`; apr_jose_jwe_make was the only one missing it.

Separately, in apr_jose_decode_compact_jwe(), the JWE tag segment's
length was computed as `dot - left`, where `dot` is the delimiter
found for the *previous* (ciphertext) segment and `left` was just
set to `dot + 1` on the line above -- so this expression is always
exactly -1. Passed as the signed slen argument to
apr_pdecode_base64_binary(), -1 is APR_ENCODE_STRING, which tells the
decoder to scan for a NUL terminator instead of respecting a length,
so it can read past the end of the real input buffer. The tag is the
last segment in Compact Serialization (no trailing delimiter), so its
length should be measured against the end of the input, `right`, not
against the previous segment's delimiter.

This was found and confirmed with a standalone reproduction: calling
apr_jose_decode() with a real decrypt callback on a well-formed JWE
compact string crashed before reaching the callback; after fixing the
first bug, the decrypt callback was reached but reported a tag length
that read past the intended segment into adjacent memory; after also
fixing the second, the reported tag length matched the actual
encoded tag exactly.

Neither released APR (<= 1.7.6) nor apr-util (<= 1.6.5) ships the
jose module; it exists only on unreleased development branches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant