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.

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.

Appendix: 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)