{"record":{"id":"4decd3126c93cd70","repo":"basecamp/fizzy","slug":"authenticator-data-is-too-short-for-attested-crede","errorCode":null,"errorMessage":"Authenticator data is too short for attested credential data","messagePattern":"Authenticator data is too short for attested credential data","errorType":"exception","errorClass":"ActionPack::WebAuthn::InvalidResponseError","httpStatus":null,"severity":"error","filePath":"lib/action_pack/web_authn/authenticator/data.rb","lineNumber":103,"sourceCode":"\n      position = 0\n\n      relying_party_id_hash = bytes[position, RELYING_PARTY_ID_HASH_LENGTH].pack(\"C*\")\n      position += RELYING_PARTY_ID_HASH_LENGTH\n\n      flags = bytes[position]\n      position += FLAGS_LENGTH\n\n      sign_count = bytes[position, SIGN_COUNT_LENGTH].pack(\"C*\").unpack1(\"N\")\n      position += SIGN_COUNT_LENGTH\n\n      aaguid = nil\n      credential_id = nil\n      public_key_bytes = nil\n\n      if flags & ATTESTED_CREDENTIAL_DATA_FLAG != 0\n        if bytes.length < position + AAGUID_LENGTH + CREDENTIAL_ID_LENGTH_BYTES\n          raise ActionPack::WebAuthn::InvalidResponseError, \"Authenticator data is too short for attested credential data\"\n        end\n\n        aaguid_bytes = bytes[position, AAGUID_LENGTH].pack(\"C*\")\n        aaguid = aaguid_bytes.unpack(\"H8H4H4H4H12\").join(\"-\")\n        position += AAGUID_LENGTH\n\n        credential_id_length = bytes[position, CREDENTIAL_ID_LENGTH_BYTES].pack(\"C*\").unpack1(\"n\")\n        position += CREDENTIAL_ID_LENGTH_BYTES\n\n        if bytes.length < position + credential_id_length + 1\n          raise ActionPack::WebAuthn::InvalidResponseError, \"Authenticator data is too short for credential ID and public key\"\n        end\n\n        credential_id = Base64.urlsafe_encode64(bytes[position, credential_id_length].pack(\"C*\"), padding: false)\n        position += credential_id_length\n\n        public_key_bytes = bytes[position..].pack(\"C*\")\n      end","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/basecamp/fizzy/blob/7aabe7458060d8a1759a53b7ede39e74e6c0b20d/lib/action_pack/web_authn/authenticator/data.rb#L85-L121","documentation":"During registration the flags byte carries the attested-credential-data bit (0x40). When that bit is set the parser needs at least 18 more bytes — a 16-byte AAGUID plus a 2-byte credential ID length field (AAGUID_LENGTH + CREDENTIAL_ID_LENGTH_BYTES). If the buffer ends first, the attested credential block is truncated and InvalidResponseError is raised.","triggerScenarios":"A registration (attestation) response whose authenticator data is exactly the 37-byte header plus a flags byte claiming attested data, but with the AAGUID/credential-ID block cut off; fabricated fixtures that set flag 0x40 without appending an attested credential payload.","commonSituations":"Test fixtures built by hand that copy flags from a real registration but strip the credential body; bugs in custom client code that reconstructs authenticatorData from parts; transport truncation of large registration payloads (credential IDs can be 100+ bytes).","solutions":["Confirm the total decoded length: registration data must exceed 37 + 18 + credential_id_length + COSE key bytes; anything close to 55 bytes with flag 0x40 set is truncated.","Send the authenticatorData buffer from the browser untouched (base64url of the whole ArrayBuffer), never a reassembled copy.","Rebuild fixtures from a real navigator.credentials.create() ceremony.","Rescue InvalidResponseError and surface a clear 'registration payload incomplete' message rather than a stack trace."],"exampleFix":"# before\n# fixture with attested flag but no attested block\nflags = 0x45 # UP | UV | AT (0x40)\ndata = ActionPack::WebAuthn::Authenticator::Data.decode(header_37_bytes_with(flags)) # raises\n\n# after — include a minimal well-formed attested block (16-byte AAGUID + 2-byte length + id + key)\nbuf = rp_id_hash_32 + [0x45].pack('C') + [0].pack('N') + aaguid_16 + [credential_id.bytesize].pack('n') + credential_id + cose_key\ndata = ActionPack::WebAuthn::Authenticator::Data.decode(buf)","handlingStrategy":"try-catch","validationCode":"raw = Base64.urlsafe_decode64(params[:authenticator_data].to_s)\nflags = raw.getbyte(32)\n# registration needs 37 + 18 bytes minimum when the attested flag (0x40) is set\nmin = (flags & 0x40).zero? ? 37 : 55\nreturn render(json: { error: 'attestation data truncated' }, status: :bad_request) if raw.bytesize < min","typeGuard":"def plausible_attestation?(raw)\n  flags = raw.getbyte(32)\n  raw.bytesize >= ((flags & 0x40).zero? ? 37 : 55)\nend","tryCatchPattern":"begin\n  attestation = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(**webauthn_params)\n  attestation.validate!\nrescue ActionPack::WebAuthn::InvalidResponseError => e\n  render json: { error: \"registration failed: #{e.message}\" }, status: :bad_request\nend","preventionTips":["Forward the browser's authenticatorData buffer untouched; never reassemble it field-by-field.","Build fixtures with a real navigator.credentials.create() capture.","Rescue InvalidResponseError at the controller boundary so malformed registrations return 400, not 500."],"tags":["webauthn","registration","attestation","binary-parsing"],"backgroundTag":"webauthn-malformed-authenticator-data","analyzedSha":"7aabe7458060d8a1759a53b7ede39e74e6c0b20d","analyzedAt":"2026-08-21T18:33:25.349Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}