Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BHttpHeaders: Private API and Validation Boundaries

Audit Haiku's private BHttpHeaders implementation, distinguish container success from HTTP validity, and validate names and values before serialization.

Haiku’s BPrivate::Network::BHttpHeaders is a useful example of why a helper named “HTTP headers” should not automatically be treated as a validating HTTP parser. The pinned implementation stores names and values, searches the list, and produces a serialized name: value string. Its success results describe its own allocation and container operations; they do not establish that an input line is valid under HTTP grammar or safe to forward on a connection.

The class is declared under headers/private/netservices/HttpHeaders.h in the BPrivate::Network namespace. That private header location is a compatibility warning: treat it as an implementation detail unless the target Haiku release documents a stable contract for your use. This is an inference from the header location and namespace, not a separate ABI guarantee stated by the implementation. The source review below is pinned to one Haiku revision; inspect the exact target source before relying on details across releases.

What the implementation actually accepts

BHttpHeader::SetHeader(const char*) clears its stored name and value, searches the input for the first colon, and returns false only when it finds no colon. When a colon exists, it trims the name, applies Haiku’s capitalization transform, and passes the bytes after the colon to SetValue. SetValue stores the string and trims its edges. Neither function performs the complete HTTP field-name token check or rejects every prohibited field-value octet.

That has a concrete parsing consequence. A line such as X-Mode : on contains whitespace before the colon, which HTTP/1.1 servers are required to reject on a received request. This container’s helper trims the name to X-Mode and can turn it into a normalized output line instead of reporting the malformed wire syntax. Likewise, a value containing embedded CR or LF is not rejected merely because it was passed through SetValue; trimming the ends does not validate the interior.

The list API has a second boundary. AddHeader(const char* line) allocates a BHttpHeader from the string and passes the resulting object to _AddOrDeleteHeader. The constructor calls SetHeader, but does not propagate its boolean result. _AddOrDeleteHeader reports whether the object was added to the list. Therefore, a true return from AddHeader(line) is not proof that the string had even one colon, much less that it satisfied HTTP syntax. It is an insertion result.

Prefer the separate name/value overload after validation. It avoids using this convenience parser as a wire-format parser, but it still does not validate the HTTP grammar for you. The API’s capitalization behavior also means the container is not a byte-for-byte preservation format: Header() builds a normalized name, colon-space separator, and value from its stored fields.

Apply protocol validation before adding data

RFC 9110 defines a field name as an HTTP token; RFC 9112 forbids whitespace between a field name and colon in HTTP/1.1 and requires a server to reject such a request. RFC 9110 also identifies field values containing CR, LF, or NUL as invalid and dangerous. Those wire-protocol requirements belong at the parser or validation layer, not in an assumption about BHttpHeaders.

The adapter below demonstrates a deliberately strict ASCII field-name check and a value check suitable for an application boundary. It rejects control bytes other than horizontal tab, including NUL, CR, LF, and DEL. This is a conservative validation policy; field-specific grammar, permitted character encoding, and application limits still need their own checks. The example includes Haiku’s private header and therefore needs the target source tree’s private include path; it is not a stable public-API example.

#include <HttpHeaders.h>
#include <string>

static bool
IsHttpTokenChar(unsigned char c)
{
	if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')
		|| (c >= 'a' && c <= 'z'))
		return true;

	const char* punctuation = "!#$%&'*+-.^_`|~";
	for (const char* p = punctuation; *p != '\0'; ++p) {
		if (c == static_cast<unsigned char>(*p))
			return true;
	}
	return false;
}

static bool
IsValidFieldName(const std::string& name)
{
	if (name.empty())
		return false;
	for (unsigned char c : name) {
		if (!IsHttpTokenChar(c))
			return false;
	}
	return true;
}

static bool
IsValidFieldValue(const std::string& value)
{
	for (unsigned char c : value) {
		if (c == '\r' || c == '\n' || c == '\0' || c == 0x7f)
			return false;
		if (c < 0x20 && c != '\t')
			return false;
	}
	return true;
}

static bool
AddValidatedHeader(BPrivate::Network::BHttpHeaders& headers,
	const std::string& name, const std::string& value)
{
	if (!IsValidFieldName(name) || !IsValidFieldValue(value))
		return false;
	return headers.AddHeader(name.c_str(), value.c_str());
}

The check iterates the whole std::string, so it catches embedded NUL instead of letting a C-string API silently see only the prefix. It also rejects whitespace in names before Haiku’s normalizer can trim it. The successful result still means only that insertion succeeded; it does not validate a field’s application-specific grammar, enforce a maximum length, resolve duplicate-field policy, or guarantee that the output is a complete HTTP message.

If input arrives from a network, first use a protocol parser that enforces message framing and the relevant HTTP version’s rules. Do not feed raw untrusted lines into SetHeader and treat a normalized result as proof that the original bytes were legal. Conversely, for a locally constructed field, validate the value and field-specific meaning before adding it. Avoid logging credentials or sensitive values such as Authorization, Cookie, or Set-Cookie during these checks.

Interpret lookup and lifetime results correctly

HasHeader(name) returns the zero-based position of the first matching item, or -1 when none is found. It is not a Boolean: index zero means that a matching header is present. Code that writes if (headers.HasHeader("Content-Type")) will treat the first entry as false and a missing entry (-1) as true. Use an explicit comparison:

const bool hasContentType = headers.HasHeader("Content-Type") >= 0;

HeaderValue(name) also returns the first match as a pointer borrowed from the stored header. Copy it if you need stable text beyond the header owner’s lifetime or across changes to that owner. HeaderAt(index) and the integer subscript operator are explicitly unchecked in the implementation: it dereferences the result of an indexed list access and requires the caller to keep the index in bounds. Check 0 <= index && index < CountHeaders() before using either form.

BHttpHeader::Header() lazily rebuilds a cached string in a const method. The implementation marks the cache mutable and changes it when the cached representation is invalid. A const signature therefore does not mean that every call is a read-only operation internally. If multiple threads may first access or mutate the same object concurrently, do not infer thread safety from the signature; establish ownership or external synchronization for the exact target version.

The container also allows repeated additions rather than applying HTTP’s field-specific combination rules. Some field definitions allow list combination; others, including Set-Cookie, have special handling. Do not merge duplicate entries generically or assume HeaderValue returns all values. Consult the field’s normative definition and preserve separate values where the protocol requires it.

Test semantic boundaries, not just happy-path insertion

Keep a small test matrix with both direct BHttpHeader behavior and list behavior. Include a normal name/value, an empty name, a name containing whitespace before a colon, a line with no colon, a value containing CRLF, an embedded NUL in a std::string, a present header at index zero, an absent name returning -1, an out-of-range index rejected by your adapter, and two repeated fields. Verify serialized output only after the validation layer accepts the input.

For an integration test, feed legal and malformed wire examples to the actual HTTP parser used by the application, not only to this container. Check that the parser rejects prohibited whitespace before a colon and prohibited CR/LF/NUL values before any forwarding or response construction. These tests establish behavior for the specific Haiku revision and protocol parser you built; they do not prove the private container is exploitable through a network path by itself.

The safe model is that BHttpHeaders is a storage and convenience layer with observable implementation details, not a complete HTTP grammar engine. Validate the original representation before normalization, distinguish an insertion result from syntax validity, handle index and pointer lifetimes explicitly, and apply each field’s actual semantics at the protocol boundary. That division lets the container remain useful without assigning it guarantees its implementation does not make.

Related:

Sources:

Comments