Skip to content

LF-only messages read as incomplete forever; bare-LF strictness is unreachable #59

Description

@MDA2AV

What happens

A header block is terminated by exactly one thing:

// UltraHardenedParser.FullHeader.ROM.cs:36
int headerEnd = span.IndexOf(ParserConstants.CrlfCrlf);
if (headerEnd < 0) return false;

So a message that uses bare LF throughout —

HTTP/1.1 200 OK\nContent-Length: 2\n\nhi

— contains no CRLFCRLF, returns false, and is reported as incomplete. The caller waits for a terminator that is never coming: until its own timeout, or until the receive buffer hits its ceiling and the request fails for an unrelated-sounding reason.

I hit this writing tests for ioxide.httpclient, which now parses responses with Glyph11. The test asserted "malformed response is refused" and instead the client hung until the harness timed it out.

The part that makes it a bug rather than a policy choice

The parser already rejects bare LF:

// FullHeader.ROM.cs:53 and :190, FullResponse.ROM.cs:55 and :142
throw new HttpParseException("Bare LF detected; only CRLF line endings are allowed.");

But those checks run after a CRLFCRLF block has been located. So:

message outcome
CRLF throughout parsed
CRLF block containing one bare LF rejected
bare LF throughout incomplete, forever

The strictness is unreachable for exactly the messages it is aimed at. A wholly LF-terminated message — the legacy case, and the one a naive or embedded origin actually produces — never reaches the rule. Only a mixed message does.

Independent of any leniency decision, false is the wrong answer here: false means "give me more bytes", and more bytes will not help. A terminated-but-malformed block should be rejected, not awaited. As it stands a peer can hold a client's connection and buffer open by sending a complete, syntactically-recognisable response that this parser will never call complete.

The leniency question

RFC 9112 §2.2 explicitly permits what is currently impossible:

Although the line terminator for the start-line and fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR.

So accepting \n is not a deviation, it is a sanctioned recipient behaviour — and for some callers it is the right one. The threat models differ by role:

  • A server parsing requests from anyone, behind or in front of other HTTP implementations wants strict. Bare LF is a smuggling primitive precisely because implementations disagree about it.
  • A client parsing a response from an origin it chose, with no intermediary, has a much weaker case for refusing a message it can unambiguously understand.

If leniency is added, one constraint matters more than the rest: it must be uniform within a message, never mixed. The attack is not "LF exists", it is "two parsers disagree about where a line ended" — which is only reachable when CRLF and bare LF appear in the same block. Accepting LF everywhere is unambiguous; accepting it sometimes is the vulnerability.

Suggested shape

  1. Fix the framing independently of policy. Find the block end as the earlier of CRLFCRLF and LFLF. If it ends with bare LFs, that is a decision (reject, or accept under 2) — never "incomplete". This alone removes the hang.
  2. Add an opt-in, e.g. ParserLimits.AllowBareLf (default false, preserving today's behaviour for the strict path). When enabled, accept LF as a terminator uniformly and still reject a block that mixes both.
  3. Apply to the request and response parsers in the same change — a client and server that disagree about line endings inside one library is the same divergence problem as bytesReadCount is one less than the bytes consumed, and the doc says otherwise #58.

Related: #57 (C core has no response parser and the diff harness is request-only), so whatever lands here needs C-side coverage before the two implementations can drift on it too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions