basecamp/fizzy · error · ActionPack::WebAuthn::InvalidCborError

Maximum nesting depth exceeded

Error message

Maximum nesting depth exceeded

What it means

The decoder tracks nesting (@depth) and refuses to recurse past MAX_DEPTH (16 by default), raising InvalidCborError. This protects the Ruby VM from stack exhaustion via deeply nested arrays/maps — a classic CBOR compression-bomb shape. Legitimate WebAuthn structures are 3–4 levels deep, so the limit only trips on malformed or hostile input unless you deliberately decode deep custom structures.

Source

Thrown at lib/action_pack/web_authn/cbor_decoder.rb:113

    def decode(bytes, **args)
      bytes = bytes.bytes if bytes.respond_to?(:bytes)
      new(bytes, **args).decode
    end
  end

  def initialize(bytes, max_depth: MAX_DEPTH, max_size: MAX_SIZE) # :nodoc:
    raise ActionPack::WebAuthn::InvalidCborError, "Input exceeds maximum size" if bytes.length > max_size

    @bytes = bytes
    @max_depth = max_depth
    @position = 0
    @depth = 0
  end

  # Decodes the next CBOR data item from the byte sequence.
  def decode
    raise ActionPack::WebAuthn::InvalidCborError, "Unexpected end of input" if @position >= @bytes.length
    raise ActionPack::WebAuthn::InvalidCborError, "Maximum nesting depth exceeded" if @depth >= @max_depth

    @depth += 1

    result = case major_type
    when UNSIGNED_INTEGER_TYPE then decode_unsigned_integer
    when NEGATIVE_INTEGER_TYPE then decode_negative_integer
    when BYTE_STRING_TYPE then decode_byte_string
    when TEXT_STRING_TYPE then decode_text_string
    when ARRAY_TYPE then decode_array
    when MAP_TYPE then decode_map
    when TAG_TYPE then decode_tag
    when FLOAT_OR_SIMPLE_TYPE then decode_float_or_simple
    end

    @depth -= 1
    result
  end

View on GitHub (pinned to 7aabe74580)

Solutions

  1. Inspect the payload's nesting before decoding in tests; anything near 16 levels in WebAuthn data is corrupt.
  2. If your own schema legitimately nests deeper, raise the cap explicitly: CborDecoder.decode(bytes, max_depth: 64).
  3. Keep the decoder off any unauthenticated endpoint; if exposed, keep default limits and fail closed.
  4. Rescue InvalidCborError and log bytesize + first bytes to identify the offending producer.

Example fix

# before
ActionPack::WebAuthn::CborDecoder.decode(user_supplied_bytes)

# after — explicit limits for a trusted, deeply-nested schema
ActionPack::WebAuthn::CborDecoder.decode(user_supplied_bytes, max_depth: 64, max_size: 1.megabyte)
Defensive patterns

Strategy: validation

Validate before calling

# for trusted schemas that legitimately nest deeper than 16
ActionPack::WebAuthn::CborDecoder.decode(bytes, max_depth: 64)
# for untrusted input, keep the default and pre-check nothing deeper than expected

Type guard

def within_cbor_depth_limit?(bytes, max_depth = ActionPack::WebAuthn::CborDecoder::MAX_DEPTH)
  # cheap structural check is not possible; rely on decode with explicit limit and rescue
  ActionPack::WebAuthn::CborDecoder.decode(bytes, max_depth: max_depth)
  true
rescue ActionPack::WebAuthn::InvalidCborError
  false
end

Try / catch

begin
  value = ActionPack::WebAuthn::CborDecoder.decode(bytes)
rescue ActionPack::WebAuthn::InvalidCborError => e
  # depth bombs and malformed data are both permanent rejections
  render json: { error: e.message }, status: :bad_request
end

Prevention

When it happens

Trigger: Decoding CBOR with more than 16 nested arrays/maps, e.g. \x82\x82\x82... repeated; hostile input crafted as a billion-laughs-style nesting bomb; custom non-WebAuthn payloads that embed deep document trees.

Common situations: Exposing CborDecoder on a public endpoint without depth caps; fuzzing runs; migrating from another CBOR gem with no/looser depth limits; legitimate deep structures in domain-specific CBOR.

Related errors


AI-assisted analysis of basecamp/fizzy@7aabe74580 (2026-08-21). Data as JSON: /api/errors/e3ec98e6bba67574. Report an issue: GitHub.