← WebKit Silent-Fix Report — 2026-W25

97990a2481  AuthenticationExtensionsClientOutputs::fromCBOR() compares a nested-map iterator against the outer map's end()

severity medium class OOB confidence 0.60 WebCore WebAuthn CBOR exploitable-grade
Charlie Wolfe Thu Jun 18 21:12:08 2026 -0700 full: 97990a2481e5152757431ac3175afd85a7284283 bug report ↗ view on GitHub ↗
Primitive: iterator from nested map compared to outer map end()
Triage note: Comparing iterators from different containers is UB and can read the wrong memory when parsing attacker-influenced CBOR.
Contents

The bug at a glance

A concrete undefined-behavior bug: fromCBOR compared an iterator from a nested credProps map against the outer decodedMap’s end(), so a credProps map lacking an ‘rk’ key passed the guard and dereferenced a past-the-end iterator. Input is attacker-influenceable CBOR from an authenticator/WebAuthn response, and dereferencing an invalid iterator is an OOB read of whatever memory follows the map’s storage. Impact is an info-leak/crash on malformed input rather than a demonstrated corruption chain, so medium.

The angle is a cross-container iterator comparison: find() on the nested map returns an iterator into that nested map, but the code compared it to decodedMap.end() – a different container. Since the two end()s are essentially never equal, the ’not found’ guard never fired, and a missing ‘rk’ key led straight to a dereference of an end/invalid iterator.

Root cause

OBSERVED: AuthenticationExtensionsClientOutputs::fromCBOR parses a CBOR map (decodedMap) of WebAuthn client extension outputs. In the credProps branch it first does ‘it = decodedMap.find(cbor::CBORValue(“credProps”))’ and checks it against decodedMap.end() correctly. Inside that branch it then reassigned the same iterator variable: ‘it = it->second.getMap().find(cbor::CBORValue(“rk”))’ – a find() on the nested credProps map, returning an iterator into the nested map.

OBSERVED: The bug is the very next line: ‘if (it != decodedMap.end() && it->second.isBool())’. It compares the nested-map iterator against the OUTER map’s end(). Comparing iterators from two different container instances is undefined behavior in C++, and practically the nested map’s end() is essentially never equal to the outer map’s end(), so the guard is effectively always true even when ‘rk’ is absent. When credProps has no ‘rk’ key, find() returns the nested map’s end(), the (wrong) comparison against decodedMap.end() does not catch it, and ‘it->second’ dereferences a past-the-end iterator.

OBSERVED: The fix introduces a helper ‘static bool booleanValue(const cbor::CBORValue::MapValue& map, ASCIILiteral key)’ that does the correct thing: ‘auto iterator = map.find(cbor::CBORValue(key)); return iterator != map.end() && iterator->second.isBool() && iterator->second.getBool();’ – it compares against the same map’s own end() and also validates the type before getBool(). The credProps branch is reduced to ‘credProps.rk = booleanValue(it->second.getMap(), “rk”_s);’. The commit notes the largeBlob branch already used a separate correctly-scoped iterator.

INFERRED: Dereferencing the end/invalid iterator of a WTF HashMap reads whatever the map’s bucket-array end sentinel points at; the concrete effect ranges from reading an uninitialized/garbage CBORValue (leading to a mis-typed value, further OOB via getBool/getMap on garbage, or a crash) to a controlled read depending on allocator state. Because the CBOR here is derived from authenticator/attestation responses, an attacker who can supply a credProps map without ‘rk’ reaches this path. The added test constructs exactly that input.

Key code

The fix: a helper comparing against the correct map’s end() and type-checking before getBool() (AuthenticationExtensionsClientOutputs.cpp)

static bool booleanValue(const cbor::CBORValue::MapValue& map, ASCIILiteral key)
{
    auto iterator = map.find(cbor::CBORValue(key));
    return iterator != map.end() && iterator->second.isBool() && iterator->second.getBool();
}
...
    it = decodedMap.find(cbor::CBORValue("credProps"));
    if (it != decodedMap.end() && it->second.isMap()) {
        CredentialPropertiesOutput credProps;
        credProps.rk = booleanValue(it->second.getMap(), "rk"_s);
        clientOutputs.credProps = credProps;
    }

Patch walkthrough

  • Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp — Adds a booleanValue() helper that looks up a key in a given map, compares the resulting iterator against that same map’s end(), and checks isBool() before getBool(). Rewrites the credProps branch to ‘credProps.rk = booleanValue(it->second.getMap(), “rk”_s);’, removing the reassignment of ‘it’ and the cross-container comparison against decodedMap.end() that dereferenced a past-the-end iterator when ‘rk’ was absent.
  • Tools/TestWebKitAPI/Tests/WebCore/CtapResponseTest.cpp — Adds encodeExtensionOutputs / encodeCredPropsExtensionOutputs CBOR-writer helpers and TEST(CTAPResponseTest, TestExtensionOutputsCredPropsWithoutRk), which encodes a credProps map that is empty and one that contains only an unknown key, then asserts fromCBOR succeeds and credProps is present – i.e. the missing-‘rk’ input no longer trips the invalid dereference.

Background

WebAuthn client extension outputs — AuthenticationExtensionsClientOutputs models the extension results returned during a WebAuthn ceremony (credProps, largeBlob, prf, etc.). fromCBOR parses these from a CBOR map, so the parser sits on the boundary between an authenticator/relying-party-influenced byte stream and structured browser state.

credProps.rk — The credProps extension reports credential properties; its ‘rk’ member indicates whether a resident key (discoverable credential) was created. It is optional, so a well-formed credProps map may legitimately omit ‘rk’ – exactly the case that triggered the bug.

CBORValue::MapValue and find() — cbor::CBORValue::MapValue is a map keyed by CBORValue. find() returns an iterator that must be compared only against that same map instance’s end(). The nested credProps map and the outer decodedMap are distinct containers with distinct end() iterators.

Cross-container iterator comparison (UB) — The C++ standard makes comparing iterators obtained from different container objects undefined behavior. Even ignoring the standard, two different maps’ end() values are unrelated addresses, so the comparison does not serve as a valid ’not found’ test and can pass when the key is genuinely absent.

Past-the-end dereference — Dereferencing end() (or an iterator that equals end()) reads the sentinel/one-past storage of the container rather than a valid element. For a hash map this reads whatever the internal table layout places there, yielding garbage, a mis-typed CBORValue, or a crash – an out-of-bounds read driven by attacker-controlled presence/absence of a key.

Vulnerability window

  1. Introduction — The credProps parsing branch reused the outer iterator variable and compared the nested-map find() result against decodedMap.end(), while the parallel largeBlob branch used a correctly-scoped iterator.
  2. Latent UB — For any credProps map without an ‘rk’ key, the guard failed to detect ’not found’ (nested end() vs outer end()), leaving a past-the-end dereference of it->second.
  3. Discovery — Bug 317240 / rdar 179858461 filed; the cross-container comparison identified as undefined behavior reachable from attacker-influenced CBOR.
  4. Fix — Introduce booleanValue() that compares against the correct map’s own end() and validates isBool() before getBool(), collapsing the credProps branch to a single safe call.
  5. Regression test — TestExtensionOutputsCredPropsWithoutRk encodes credProps maps with no ‘rk’ (empty, and unknown-key-only) and asserts fromCBOR succeeds with credProps present.
  6. Landed — Committed as 315510@main on 2026-06-18, reviewed by Pascoe and Darin Adler.

Proof of concept

OBSERVED: This is the verbatim added TestWebKitAPI test plus its two CBOR-writer helpers. It encodes a WebAuthn extension-outputs CBOR map whose ‘credProps’ entry is a map with no ‘rk’ key (first: empty map; second: only an ‘unknownKey’), feeds it to AuthenticationExtensionsClientOutputs::fromCBOR, and asserts the parse succeeds with credProps present. Pre-patch the missing ‘rk’ key made the nested find() return the credProps map’s end(), which the buggy comparison against decodedMap.end() failed to catch, leading to a past-the-end dereference; the test exercises exactly that input to lock in the fix.

static Vector<uint8_t> encodeExtensionOutputs(cbor::CBORValue::MapValue&& map)
{
    auto encoded = cbor::CBORWriter::write(cbor::CBORValue(WTF::move(map)));
    return encoded.value();
}

static Vector<uint8_t> encodeCredPropsExtensionOutputs(cbor::CBORValue::MapValue&& credPropsMap)
{
    cbor::CBORValue::MapValue root;
    root[cbor::CBORValue("credProps")] = cbor::CBORValue(WTF::move(credPropsMap));
    return encodeExtensionOutputs(WTF::move(root));
}

TEST(CTAPResponseTest, TestExtensionOutputsCredPropsWithoutRk)
{
    {
        auto outputs = AuthenticationExtensionsClientOutputs::fromCBOR(encodeCredPropsExtensionOutputs({ }));
        ASSERT_TRUE(outputs);
        EXPECT_TRUE(outputs->credProps.has_value());
    }

    {
        cbor::CBORValue::MapValue credProps;
        credProps[cbor::CBORValue("unknownKey")] = cbor::CBORValue(true);
        auto outputs = AuthenticationExtensionsClientOutputs::fromCBOR(encodeCredPropsExtensionOutputs(WTF::move(credProps)));
        ASSERT_TRUE(outputs);
        EXPECT_TRUE(outputs->credProps.has_value());
    }
}

Exploitation

  1. Deliver input — Cause fromCBOR to parse a client-extension-outputs CBOR blob containing a credProps map that omits ‘rk’ – reachable via authenticator/attestation responses processed by the WebAuthn stack.
  2. Trigger UB — The nested find() returns the credProps map’s end(); the pre-patch comparison against the outer decodedMap.end() does not detect it, so it->second dereferences a past-the-end iterator, an OOB read of the map’s table storage.
  3. Outcome — Practically a mis-typed/garbage CBORValue read leading to incorrect parsing or a crash; a controlled information leak would require shaping allocator/table layout so the sentinel read returns attacker-useful bytes, which the patch does not demonstrate. Treat as an attacker-triggerable OOB read / crash on malformed input.

Detection & hunting

For defenders and SOC / detection engineers:

  • credProps without rk in WebAuthn responses
  • Crashes in AuthenticationExtensionsClientOutputs::fromCBOR
  • CBOR parser fuzzing

Audit directions

  • Other nested-map lookups in webauthn parsing
  • Iterator variable reuse
  • Type-check before getters
  • Attacker-reachable CBOR surfaces

Before / after

Loading diff…