Empty NEW_TOKEN token is not rejected on receipt
Summary
RFC 9000 Section 19.7 requires the Token field in a NEW_TOKEN frame to be non-empty. It also 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 the reviewed quiche code path, an empty token is parsed as a valid QuicNewTokenFrame and delivered to the connection visitor. The upper TLS client/cache path later ignores the empty token, but the connection is not closed and no receive-side connection error is raised.
Standard Requirement
Official standard: https://www.rfc-editor.org/rfc/rfc9000.html#section-19.7
Section: RFC 9000 Section 19.7, NEW_TOKEN Frames
The token MUST NOT be empty.
The same paragraph requires the client to handle receipt of an empty Token field in a NEW_TOKEN frame as a connection error of type FRAME_ENCODING_ERROR. RFC 9000 Section 11 leaves room for implementations to choose the most applicable error code in some cases, but the important behavior here is that the client should reject the received frame as a connection error instead of accepting and delivering it normally.
Relevant Source Code
Frame Parsing
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3082
case IETF_NEW_TOKEN: {
QuicNewTokenFrame frame;
if (!ProcessNewTokenFrame(reader, &frame)) {
return RaiseError(QUIC_INVALID_NEW_TOKEN);
}
QUIC_DVLOG(2) << ENDPOINT << "Processing IETF new token frame "
<< frame;
if (!visitor_->OnNewTokenFrame(frame)) {
QUIC_DVLOG(1) << "Visitor asked to stop further processing.";
// Returning true since there was no parsing error.
return true;
}
break;
}
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:5195
bool QuicFramer::ProcessNewTokenFrame(QuicDataReader* reader,
QuicNewTokenFrame* frame) {
uint64_t length;
if (!reader->ReadVarInt62(&length)) {
set_detailed_error("Unable to read new token length.");
return false;
}
if (length > kMaxNewTokenTokenLength) {
set_detailed_error("Token length larger than maximum.");
return false;
}
// TODO(ianswett): Don't use absl::string_view as an intermediary.
absl::string_view data;
if (!reader->ReadStringPiece(&data, length)) {
set_detailed_error("Unable to read new token data.");
return false;
}
frame->token = std::string(data);
return true;
}
ProcessNewTokenFrame() checks whether the length field can be read, whether the length exceeds the configured maximum, and whether the token bytes can be read. It does not reject length == 0. With a zero-length token, ReadStringPiece(&data, 0) can succeed, frame->token becomes an empty string, and the function returns true.
Client Receive Path
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2162
bool QuicConnection::OnNewTokenFrame(const QuicNewTokenFrame& frame) {
QUIC_BUG_IF(quic_bug_12714_15, !connected_)
<< "Processing NEW_TOKEN frame when connection is closed. Received "
"packet info: "
<< last_received_packet_info_;
if (!UpdatePacketContent(NEW_TOKEN_FRAME)) {
return false;
}
if (debug_visitor_ != nullptr) {
debug_visitor_->OnNewTokenFrame(frame);
}
if (perspective_ == Perspective::IS_SERVER) {
CloseConnection(QUIC_INVALID_NEW_TOKEN, "Server received new token frame.",
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return false;
}
// NEW_TOKEN frame should insitgate ACKs.
MaybeUpdateAckTimeout();
visitor_->OnNewTokenReceived(frame.token);
return true;
}
The client branch does not check frame.token.empty(). It updates ACK state, calls visitor_->OnNewTokenReceived(frame.token), and returns true.
Upper-Layer Handling
quiche-main/quiche-main/quiche/quic/core/tls_client_handshaker.cc:512
void TlsClientHandshaker::OnNewTokenReceived(absl::string_view token) {
if (token.empty()) {
return;
}
if (session_cache_ != nullptr) {
session_cache_->OnNewTokenReceived(server_id_, token);
}
}
quiche-main/quiche-main/quiche/quic/core/crypto/quic_client_session_cache.cc:114
void QuicClientSessionCache::OnNewTokenReceived(const QuicServerId& server_id,
absl::string_view token) {
if (token.empty()) {
return;
}
auto iter = cache_.Lookup(server_id.cache_key());
if (iter == cache_.end()) {
return;
}
The TLS client handshaker and client session cache ignore empty tokens after delivery. That prevents an empty token from being cached, but it is not the receive-side connection error required by RFC 9000.
Existing Test Expectation
quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:14323
QuicNewTokenFrame* new_token = new QuicNewTokenFrame();
EXPECT_CALL(visitor_, OnNewTokenReceived(_));
ProcessFramePacket(QuicFrame(new_token));
// Ensure that this has caused the ACK alarm to be set.
EXPECT_TRUE(connection_.HasPendingAcks());
quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:16105
EXPECT_CALL(visitor_, OnNewTokenReceived(""));
The existing tests exercise the empty-token delivery behavior. They expect the visitor callback to receive an empty token instead of expecting the connection to close.
Error-Code Mapping
quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:580
case QUIC_INVALID_NEW_TOKEN:
return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};
If ProcessNewTokenFrame() were to fail and raise QUIC_INVALID_NEW_TOKEN, that internal error currently maps to PROTOCOL_VIOLATION, not FRAME_ENCODING_ERROR. The main issue in this receive path, however, is earlier: an empty token does not trigger RaiseError() or CloseConnection() at all.
Additional Path Checks
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:5753
bool QuicConnection::UpdatePacketContent(QuicFrameType type) {
last_received_packet_info_.frames.push_back(type);
if (version().IsIetfQuic()) {
if (perspective_ == Perspective::IS_CLIENT) {
return connected_;
}
UpdatePacketContent(NEW_TOKEN_FRAME) does not validate token contents on the client side. In the IETF QUIC client path, it records the frame type and returns the current connection state.
quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:809
void ProcessFramePacket(QuicFrame frame) {
ProcessFramePacketWithAddresses(frame, kSelfAddress, kPeerAddress,
ENCRYPTION_FORWARD_SECURE);
}
quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:927
std::unique_ptr<QuicPacket> packet(ConstructPacket(header, frames));
char buffer[kMaxOutgoingPacketSize];
size_t encrypted_length =
peer_framer_.EncryptPayload(level, QuicPacketNumber(number), *packet,
buffer, kMaxOutgoingPacketSize);
connection_.ProcessUdpPacket(
kSelfAddress, kPeerAddress,
QuicReceivedPacket(buffer, encrypted_length, clock_.Now(), false, 0,
true, nullptr, 0, false, ecn_codepoint));
The QuicConnectionTest helper does not simply call OnNewTokenFrame() directly. It constructs a packet, encrypts it, and feeds it through ProcessUdpPacket(), which supports the conclusion that empty-token delivery is part of the packet receive path.
Searches for empty-token handling did not find a client-side FRAME_ENCODING_ERROR path for empty NEW_TOKEN. The QUIC_INVALID_NEW_TOKEN sites are limited to framer parse failure, server receipt of a NEW_TOKEN frame, error-code definitions, and test boundary checks.
Implementation Behavior
The reviewed path is:
IETF_NEW_TOKEN reaches ProcessNewTokenFrame().
- With token length 0, the length field is read successfully and is not larger than the maximum.
ReadStringPiece(&data, 0) succeeds.
frame->token is set to an empty string.
ProcessNewTokenFrame() returns true, and the framer calls visitor_->OnNewTokenFrame(frame).
- The client-side
QuicConnection::OnNewTokenFrame() does not close the connection. It calls visitor_->OnNewTokenReceived(frame.token) and returns true.
- The TLS handshaker/cache layer ignores the empty token, but it does not convert the received frame into a connection error.
Inconsistency Reason
The standard requires a client that receives a NEW_TOKEN frame with an empty Token field to treat that receipt as a connection error. quiche instead parses the frame successfully, delivers the empty string to the connection visitor, and keeps the connection path alive.
The relevant mismatch spans both the parsing layer and the client connection layer: the parser does not reject length == 0, and the client connection handler does not close the connection when it receives an empty token.
Source Evidence Summary
quiche_framer.cc:5195 to quiche_framer.cc:5214: NEW_TOKEN parsing lacks a length == 0 check.
quiche_framer.cc:3082 to quiche_framer.cc:3094: after successful parsing, the framer calls visitor_->OnNewTokenFrame(frame).
quiche_connection.cc:2162 to quiche_connection.cc:2182: the client path passes frame.token to OnNewTokenReceived() without closing the connection.
quiche_connection.cc:5753 to quiche_connection.cc:5757: UpdatePacketContent() does not intercept empty NEW_TOKEN frames on the client path.
tls_client_handshaker.cc:512 to tls_client_handshaker.cc:519: the upper layer ignores an empty token after delivery.
quic_connection_test.cc:14323 to quic_connection_test.cc:14328: a default empty QuicNewTokenFrame is expected to trigger the visitor and ACK behavior.
quic_connection_test.cc:16105: the test expects OnNewTokenReceived("").
quic_connection_test.cc:927 to quic_connection_test.cc:936: the test helper enters the receive path through an encrypted packet and ProcessUdpPacket().
Impact
A peer can send an empty NEW_TOKEN frame to a quiche client without causing the RFC-required connection error. The cache layer ignores the empty token, so the immediate security impact is limited: the empty token is not stored for later Initial packets. The primary impact is incorrect protocol error handling, weaker conformance behavior, and possible failure in strict interoperability or compliance tests.
Fix Direction
Reject empty tokens explicitly either in the parser or in the client receive path. The most direct parser-side change is to add a length == 0 check in ProcessNewTokenFrame() after the token length is read, then return an error that maps to an appropriate IETF transport error. A connection-side implementation would need to call CloseConnection() in the client branch before the empty token can reach OnNewTokenReceived().
Empty NEW_TOKEN token is not rejected on receipt
Summary
RFC 9000 Section 19.7 requires the
Tokenfield in aNEW_TOKENframe to be non-empty. It also requires a client that receives aNEW_TOKENframe with an emptyTokenfield to treat it as a connection error of typeFRAME_ENCODING_ERROR.In the reviewed quiche code path, an empty token is parsed as a valid
QuicNewTokenFrameand delivered to the connection visitor. The upper TLS client/cache path later ignores the empty token, but the connection is not closed and no receive-side connection error is raised.Standard Requirement
Official standard: https://www.rfc-editor.org/rfc/rfc9000.html#section-19.7
Section: RFC 9000 Section 19.7, NEW_TOKEN Frames
The same paragraph requires the client to handle receipt of an empty
Tokenfield in aNEW_TOKENframe as a connection error of typeFRAME_ENCODING_ERROR. RFC 9000 Section 11 leaves room for implementations to choose the most applicable error code in some cases, but the important behavior here is that the client should reject the received frame as a connection error instead of accepting and delivering it normally.Relevant Source Code
Frame Parsing
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3082quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:5195ProcessNewTokenFrame()checks whether the length field can be read, whether the length exceeds the configured maximum, and whether the token bytes can be read. It does not rejectlength == 0. With a zero-length token,ReadStringPiece(&data, 0)can succeed,frame->tokenbecomes an empty string, and the function returnstrue.Client Receive Path
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2162The client branch does not check
frame.token.empty(). It updates ACK state, callsvisitor_->OnNewTokenReceived(frame.token), and returnstrue.Upper-Layer Handling
quiche-main/quiche-main/quiche/quic/core/tls_client_handshaker.cc:512quiche-main/quiche-main/quiche/quic/core/crypto/quic_client_session_cache.cc:114The TLS client handshaker and client session cache ignore empty tokens after delivery. That prevents an empty token from being cached, but it is not the receive-side connection error required by RFC 9000.
Existing Test Expectation
quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:14323quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:16105The existing tests exercise the empty-token delivery behavior. They expect the visitor callback to receive an empty token instead of expecting the connection to close.
Error-Code Mapping
quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:580If
ProcessNewTokenFrame()were to fail and raiseQUIC_INVALID_NEW_TOKEN, that internal error currently maps toPROTOCOL_VIOLATION, notFRAME_ENCODING_ERROR. The main issue in this receive path, however, is earlier: an empty token does not triggerRaiseError()orCloseConnection()at all.Additional Path Checks
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:5753UpdatePacketContent(NEW_TOKEN_FRAME)does not validate token contents on the client side. In the IETF QUIC client path, it records the frame type and returns the current connection state.quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:809quiche-main/quiche-main/quiche/quic/core/quic_connection_test.cc:927The
QuicConnectionTesthelper does not simply callOnNewTokenFrame()directly. It constructs a packet, encrypts it, and feeds it throughProcessUdpPacket(), which supports the conclusion that empty-token delivery is part of the packet receive path.Searches for empty-token handling did not find a client-side
FRAME_ENCODING_ERRORpath for emptyNEW_TOKEN. TheQUIC_INVALID_NEW_TOKENsites are limited to framer parse failure, server receipt of aNEW_TOKENframe, error-code definitions, and test boundary checks.Implementation Behavior
The reviewed path is:
IETF_NEW_TOKENreachesProcessNewTokenFrame().ReadStringPiece(&data, 0)succeeds.frame->tokenis set to an empty string.ProcessNewTokenFrame()returnstrue, and the framer callsvisitor_->OnNewTokenFrame(frame).QuicConnection::OnNewTokenFrame()does not close the connection. It callsvisitor_->OnNewTokenReceived(frame.token)and returnstrue.Inconsistency Reason
The standard requires a client that receives a
NEW_TOKENframe with an emptyTokenfield to treat that receipt as a connection error. quiche instead parses the frame successfully, delivers the empty string to the connection visitor, and keeps the connection path alive.The relevant mismatch spans both the parsing layer and the client connection layer: the parser does not reject
length == 0, and the client connection handler does not close the connection when it receives an empty token.Source Evidence Summary
quiche_framer.cc:5195toquiche_framer.cc:5214:NEW_TOKENparsing lacks alength == 0check.quiche_framer.cc:3082toquiche_framer.cc:3094: after successful parsing, the framer callsvisitor_->OnNewTokenFrame(frame).quiche_connection.cc:2162toquiche_connection.cc:2182: the client path passesframe.tokentoOnNewTokenReceived()without closing the connection.quiche_connection.cc:5753toquiche_connection.cc:5757:UpdatePacketContent()does not intercept emptyNEW_TOKENframes on the client path.tls_client_handshaker.cc:512totls_client_handshaker.cc:519: the upper layer ignores an empty token after delivery.quic_connection_test.cc:14323toquic_connection_test.cc:14328: a default emptyQuicNewTokenFrameis expected to trigger the visitor and ACK behavior.quic_connection_test.cc:16105: the test expectsOnNewTokenReceived("").quic_connection_test.cc:927toquic_connection_test.cc:936: the test helper enters the receive path through an encrypted packet andProcessUdpPacket().Impact
A peer can send an empty
NEW_TOKENframe to a quiche client without causing the RFC-required connection error. The cache layer ignores the empty token, so the immediate security impact is limited: the empty token is not stored for later Initial packets. The primary impact is incorrect protocol error handling, weaker conformance behavior, and possible failure in strict interoperability or compliance tests.Fix Direction
Reject empty tokens explicitly either in the parser or in the client receive path. The most direct parser-side change is to add a
length == 0check inProcessNewTokenFrame()after the token length is read, then return an error that maps to an appropriate IETF transport error. A connection-side implementation would need to callCloseConnection()in the client branch before the empty token can reachOnNewTokenReceived().