Skip to content

Empty NEW_TOKEN Is Accepted Instead of FRAME_ENCODING_ERROR #135

@LiD0209

Description

@LiD0209

Empty NEW_TOKEN Is Accepted Instead of FRAME_ENCODING_ERROR

Summary

RFC 9000 requires a client that receives a NEW_TOKEN frame with an empty Token field to treat it as a connection error of type FRAME_ENCODING_ERROR.

In quiche, Token Length == 0 is parsed successfully as an empty string and forwarded through the client connection path. The connection remains open, and the empty token is ignored later by the TLS client handshaker instead of causing the required transport error.

Standard Requirement

An opaque blob that the client can use with a future Initial packet. The token
MUST NOT be empty. A client MUST treat receipt of a NEW_TOKEN frame with an
empty Token field as a connection error of type FRAME_ENCODING_ERROR.

The triggering condition is receipt of a NEW_TOKEN frame whose Token field is empty. The required outcome is a client connection error using FRAME_ENCODING_ERROR.

Relevant Source Code

Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3082-3090

3082:     case IETF_NEW_TOKEN: {
3083:       QuicNewTokenFrame frame;
3084:       if (!ProcessNewTokenFrame(reader, &frame)) {
3085:         return RaiseError(QUIC_INVALID_NEW_TOKEN);
3086:       }
3087:       QUIC_DVLOG(2) << ENDPOINT << "Processing IETF new token frame " << frame;
3088:       if (!visitor_->OnNewTokenFrame(frame)) {
3089:         QUIC_DVLOG(1) << "Visitor asked to stop further processing.";
3090:         return true;

After successful parsing, the framer delivers the NEW_TOKEN frame to the visitor.

Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:5195-5214

5195: bool QuicFramer::ProcessNewTokenFrame(QuicDataReader* reader,
5196:                                       QuicNewTokenFrame* frame) {
5197:   uint64_t length;
5198:   if (!reader->ReadVarInt62(&length)) {
5199:     set_detailed_error("Unable to read new token length.");
5200:     return false;
5201:   }
5202:   if (length > kMaxNewTokenTokenLength) {
5203:     set_detailed_error("Token length larger than maximum.");
5204:     return false;
5205:   }
5206:
5207:   absl::string_view data;
5208:   if (!reader->ReadStringPiece(&data, length)) {
5209:     set_detailed_error("Unable to read new token data.");
5210:     return false;
5211:   }
5212:   frame->token = std::string(data);
5213:   return true;
5214: }

ProcessNewTokenFrame() does not reject length == 0. A zero-length token parses successfully and becomes frame->token == "".

Source: quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2162-2182

2162: bool QuicConnection::OnNewTokenFrame(const QuicNewTokenFrame& frame) {
2163:   QUIC_BUG_IF(quic_bug_12714_15, !connected_)
2164:       << "Processing NEW_TOKEN frame when connection is closed. Received "
2165:          "packet info: "
2166:       << last_received_packet_info_;
2167:   if (!UpdatePacketContent(NEW_TOKEN_FRAME)) {
2168:     return false;
2169:   }
2170:
2171:   if (debug_visitor_ != nullptr) {
2172:     debug_visitor_->OnNewTokenFrame(frame);
2173:   }
2174:   if (perspective_ == Perspective::IS_SERVER) {
2175:     CloseConnection(QUIC_INVALID_NEW_TOKEN, "Server received new token frame.",
2176:                     ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
2177:     return false;
2178:   }
2179:   MaybeUpdateAckTimeout();
2180:   visitor_->OnNewTokenReceived(frame.token);
2181:   return true;
2182: }

The client path does not check frame.token.empty() before delivering the token to the visitor.

Source: quiche-main/quiche-main/quiche/quic/core/tls_client_handshaker.cc:512-518

512: void TlsClientHandshaker::OnNewTokenReceived(absl::string_view token) {
513:   if (token.empty()) {
514:     return;
515:   }
516:   if (session_cache_ != nullptr) {
517:     session_cache_->OnNewTokenReceived(server_id_, token);
518:   }

The TLS client handshaker ignores an empty token after it has already been accepted by the connection layer.

Source: quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:580-581

580:     case QUIC_INVALID_NEW_TOKEN:
581:       return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};

The generic QUIC_INVALID_NEW_TOKEN mapping is PROTOCOL_VIOLATION, not FRAME_ENCODING_ERROR.

Implementation Behavior

When a client receives an IETF NEW_TOKEN frame with Token Length == 0, quiche parses the frame successfully, stores an empty string in frame.token, and delivers it through QuicConnection::OnNewTokenFrame().

The connection layer does not close the connection. The default TLS client handshaker later ignores the empty token and does not cache it.

Inconsistency Reason

The standard requires an empty NEW_TOKEN token to cause a client connection error of type FRAME_ENCODING_ERROR.

quiche treats the empty token as a valid parsed frame and only ignores it after visitor delivery. Ignoring the token at the handshaker layer is not equivalent to generating the required transport error.

Impact

The default client does not cache the empty token, so the direct security impact is limited. The observable issue is protocol behavior: a peer or conformance test expects FRAME_ENCODING_ERROR, but quiche keeps the connection open and silently drops the token later.

Fix Direction

Reject empty NEW_TOKEN tokens on the client receive path.

After reading the token length, if length == 0, quiche should close the connection with an IETF transport error of FRAME_ENCODING_ERROR. The fix should preserve the existing server-side behavior for receiving a NEW_TOKEN frame, which can still be treated as a protocol violation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions