# Can HTTP request injection start with a line break?

A credential injector can keep an API key out of an agent's context and still send that key in the wrong request. The failure happens when the gateway treats agent input as harmless text, then pastes it into HTTP syntax after the credential layer has done its job.

I have seen teams put real effort into vault storage, approval prompts, and audit logs, then leave a string formatter between the agent and the wire. That formatter becomes part of the security boundary. If it accepts a carriage return or newline in a destination, a custom header name, or an agent-supplied header value, it can let data create request structure.

The fix is deliberately boring: reject carriage return and newline before request rendering, parse structured inputs instead of composing them with strings, and test the bytes that leave the process. Do not try to sanitize this with trimming or replacement. A rejected request is honest. A repaired request can be a different request.

## Can HTTP request injection start with a line break?

Yes. In HTTP/1.1, a line break separates parts of the message. If code places untrusted text into a request line or header line without enforcing that boundary, an attacker can use CRLF, carriage return followed by newline, to terminate the current line and begin another one.

A simplified vulnerable renderer often looks harmless:

```text
GET {path} HTTP/1.1\r\n
Host: {host}\r\n
Authorization: Bearer {vault_token}\r\n
{custom_name}: {agent_value}\r\n
\r\n
```

Suppose the agent supplies this value:

```text
blue\r\nX-Forwarded-Host: internal.example
```

The rendered bytes now contain an extra header. The credential did not leak into the agent's prompt. The agent still influenced the authenticated request in a way the designer did not intend.

The same class of bug appears in a destination string when a gateway constructs the first line itself, or when one layer parses a URL and another later combines a path, host, or proxy target through text formatting. The exact outcome depends on the HTTP library and intermediary behavior. That uncertainty is not a defense. Different components disagreeing about message boundaries is how a small validation omission becomes a security incident.

RFC 9110 defines HTTP field lines as syntax made from a field name, colon, and field value. It also treats line breaks as message framing, not ordinary content inside a field. That distinction matters more than a generic warning to "escape user input." There is no useful escaped form of a line break inside a header field for this purpose. Reject it.

## A vault protects credentials, not request shape

Credential injection and request injection solve separate problems. The first keeps the secret away from the agent. The second ensures that the request which receives the secret has the shape the human approved.

Consider a tool that allows an agent to call a billing API with a vault-held bearer token and accepts an optional custom header. A developer may reason that the header is low risk because the token never appears in the tool arguments. That reasoning overlooks adjacency. An injected line can add a forwarding header, alter a content length, duplicate an application header, or poison an audit label used downstream. The credential remains secret while the action becomes unsafe.

Destinations deserve the same suspicion. A hostname that looks like data may become authority syntax. A path may become a request target. A proxy selector can change where the client connects. If the agent can influence any of those values, the gateway must decide what forms are valid before it attaches a credential.

This is why an allowlist of approved hostnames is useful but insufficient. It limits where a parsed URL may point. It does not prove that every component received the same parsed URL, nor does it stop line breaks in user info, a header override, or a string appended after the allowlist check. The boundary must reject control characters and preserve structured values all the way to the HTTP client.

## Reject CR and LF before any request object exists

The safest rule is small enough to state in one sentence: agent-controlled destination fields, custom header names, and custom header values must contain neither `\r` nor `\n` after decoding.

Apply that rule before you build a URL, construct headers, choose a client option, or write a log entry that later feeds a request retry. Check the original string to catch literal control characters. Decode according to the input contract once, then check the decoded string again. Do not repeatedly decode until nothing changes. Repeated decoding turns a clear contract into guesswork and can produce surprising results.

A compact validation function has the right shape when it refuses to repair input:

```text
validateRequestText(field, value):
  if value contains "\r" or "\n":
    fail(field + " contains a line break")
  return value
```

In real code, validate the language's actual string value, not a printed representation. A JSON payload may contain `"\n"`; after JSON parsing, that becomes one newline character. Checking for the two visible characters backslash and n misses the dangerous value.

Do not call `trim()` and continue. Trimming removes only edges in most implementations, so a line break in the middle survives. Replacing line breaks with spaces is worse in a different way: it changes the requested action while leaving the operator with no clear record that the agent asked for forbidden syntax. Treat the input as invalid and stop before network I/O.

The rejection response should identify the field but avoid reflecting sensitive content. `custom header value contains a line break` tells the caller enough. Echoing the complete value may place credentials or private data into a terminal transcript.

## Header names need a narrower contract than values

A header value can legitimately contain spaces and many visible characters. A header name should not. Letting an agent select arbitrary names creates more ambiguity than most action gateways need.

Use a conservative name rule: ASCII letters, digits, and hyphen only, with a reasonable length limit. Reject colon, whitespace, control characters, and non-ASCII bytes unless you have a documented reason to support a specific extension. The goal is not to imitate every permissive parser on the internet. The goal is to produce one unambiguous request.

This input must fail even though its apparent value is ordinary:

```json
{
  "name": "X-Trace\r\nAuthorization",
  "value": "debug"
}
```

So must this one:

```json
{
  "name": "X-Trace: injected",
  "value": "debug"
}
```

A colon belongs to the renderer, not to the supplied name. If the gateway accepts it, one parser may see a name while another sees a complete field line. A header name with leading whitespace invites similar disagreement.

Header values need their own policy beyond CRLF. Decide whether the gateway accepts repeated headers, whether it permits commas, and which headers agents may override. Do not merge duplicate names by joining text unless the header's semantics explicitly allow it. `Set-Cookie`, `Content-Length`, `Host`, `Authorization`, and forwarding headers all deserve special handling or a hard deny. I prefer gateways that reserve protocol and credential headers entirely, then offer a small set of application headers agents may supply.

That recommendation sometimes gets pushback because arbitrary custom headers make a generic HTTP tool feel flexible. Flexibility is popular because it avoids adding a tool option when an API wants one more header. It is still the wrong default for credentialed agent traffic. A named option such as `idempotency_key` or `request_id` has a clear format and an owner. An unrestricted header bag has neither.

## Parse destinations once and keep their parts structured

A destination is not a string after the gateway accepts it. It is a structured value with a scheme, host, port, path, query, and sometimes user info or a fragment. Parse it once with a standards-aware URL parser, reject disallowed components, then hand the parsed value to the HTTP client without converting it back to a template string.

The exact allowlist depends on the action. A tool that calls one vendor API may require HTTPS, one hostname, and port 443. A broader tool may allow several preconfigured origins. In either case, reject embedded credentials, fragments, malformed hostnames, and any decoded carriage return or newline before the request is created. Fragments do not go on the wire, but accepting them adds needless confusion to logs and approval screens.

A useful request boundary looks like this:

```text
input destination
  -> decode once
  -> reject CR and LF
  -> parse URL
  -> require HTTPS
  -> require allowed host and allowed port
  -> reject user info and fragment
  -> pass parsed URL to HTTP client
```

The ordering is deliberate. A host allowlist applied to a raw string can be fooled by user info such as `https://allowed.example@other.example/`. A line-break check applied only after a library has normalized a malformed value may miss what the earlier parser accepted. Parse first for meaning, but reject forbidden control characters before any component may render them as syntax.

Do not build an HTTP request by concatenating `scheme + "://" + host + path`. That pattern turns a structured value back into text at the moment you most need the parser's guarantees. If a library requires separate host and path arguments, validate each argument under the same control-character rule and use the library's structured API.

## Encodings create bypasses when layers disagree

Literal CRLF is the test everyone remembers. Encoded CRLF is the test that finds the real bug.

An agent may send `%0d%0a` in a URL component. If the gateway checks the raw string and later URL-decodes it before using a custom renderer, the check has protected the wrong representation. The resulting line break appears after validation. Double encoding, such as `%250d%250a`, becomes relevant if different layers decode separately.

Choose an explicit normalization boundary. For example, parse JSON once, percent-decode only where the URL standard requires it, validate the representation your renderer will consume, and prohibit later generic decoding. Keep a test for every transition. This is less glamorous than a large sanitizer, but it gives reviewers something they can reason about.

A test table should cover at least these inputs for each agent-controlled field:

- a literal `\r`, a literal `\n`, and their adjacent CRLF form
- `%0d`, `%0a`, and `%0d%0a` where percent encoding is accepted
- `%250d%250a` if another component might decode later
- a header name containing colon or whitespace
- a destination with user info, an unexpected port, or an unapproved host

Do not test only for an error message. Assert that the client transport never runs for rejected input. A validator that reports an error after constructing or queuing the request has already failed the security property you care about.

There is a related trap in configuration formats. Environment variables, YAML, JSON, and shell arguments each have their own escaping rules. A test fixture that visually contains `\n` may contain two harmless characters, while production JSON decoding turns it into a newline. Build tests through the same parsing path that accepts real agent calls.

## The failure usually hides in convenience code

The dangerous code often arrives after the initial security review. Someone adds custom headers for a new API, makes a debug proxy configurable, or writes a retry function that reconstructs a request for logging. Each change is individually reasonable. Together, they can reintroduce string rendering after the original gateway had used safe client APIs.

I have found this by tracing one action from input to socket, not by searching only for the word `header`. Look for string interpolation around `Host:`, `Authorization:`, `Cookie:`, `Content-Length:`, request targets, proxy commands, raw HTTP test helpers, and log replay utilities. Search for decode functions too. A safe validator near the entry point does little if a later path accepts a different representation.

A representative failure has four parts. The gateway validates a destination host against an allowlist. It stores the rest of the destination as text for a low-level client. A retry helper percent-decodes that text to make logs easier to read. The helper then writes a raw request line for the retry, and `%0d%0aX-Test:%20yes` has become message syntax.

None of those lines says "allow request injection." The fault is the missing invariant: untrusted content must never contain line-break characters when code renders an HTTP message. Put that invariant in one shared validator and make raw rendering an exception that tests must justify.

If you use a higher-level HTTP library, keep it high-level all the way through. Avoid falling back to a raw socket to support one odd endpoint. If an API genuinely demands an unusual wire format, give that integration a dedicated implementation and a narrow input model. Do not weaken the generic path for it.

## Logs must show rejected intent without repeating secrets

When validation rejects a request, record enough context to investigate it: time, agent session or process identity, action name, field category, rejection reason, and a safe digest or length of the supplied value. Do not store the full header value by default. Headers often contain tokens, personal data, or signed material that becomes another secret-management problem once copied into logs.

Separate the audit event for an attempted action from the event for an executed action. A rejected request should not look like a failed network call. It never reached the network. That distinction helps incident response and prevents operators from chasing a remote service that never saw the request.

Sallyport keeps a Sessions journal for agent runs and an Activity journal for calls, both projected from its encrypted hash-chained audit log. That gives a gateway a sensible place to show a refused action, but the input rule still belongs before HTTP rendering.

The offline verification property matters here. `sp audit verify` can verify the chain over ciphertext without the vault key, which can establish that recorded events have not been altered. It cannot tell you whether a validator was too permissive. The event schema and tests must make the rejected field category visible without preserving dangerous data.

## Test the serialized request, not just the validator

Unit tests for `contains('\r') || contains('\n')` are necessary and insufficient. They prove one function rejects two characters. They do not prove the actual transport cannot receive a line break introduced by decoding, defaults, retries, or a custom adapter.

Use a local test server or a recording transport that captures the outgoing request object and, where your stack permits it, its serialized bytes. Feed approved inputs through the same tool entry point agents use. Assert the method, authority, path, and complete header set. Then feed rejected cases and assert that the recording transport saw zero requests.

A compact test matrix catches more regressions than a long collection of vague security tests:

```text
field                 input                         expected result
custom header value   "ok\r\nX-Added: yes"          reject before transport
custom header name    "X-Mode: injected"            reject before transport
destination path      "/v1/a%0d%0aX-Added:%20yes"   reject after decoding
allowed destination   "https://api.example/v1/a"    one request, expected host
```

Add property tests if the language makes them practical. Generate control characters around every accepted character class, then assert rejection. Fuzzing is helpful only after the contract is crisp. A fuzzer will find strange parser cases; it cannot tell you whether arbitrary header names were a product decision or an accident.

When a test fails, inspect the final bytes before changing the validator. I have watched teams add more replacements until a case passed, only to discover that the client had folded whitespace or decoded twice. The right repair is usually to remove the formatter or decoder that made raw syntax possible.

## Human approval does not excuse malformed actions

A per-session approval answers whether a particular agent process may act. A per-call approval answers whether this use of a protected credential deserves a human decision. Neither makes an ambiguous request safe. A person cannot reliably inspect hidden control characters in a long destination or header value, and approval fatigue will turn a subtle prompt into a rubber stamp.

Keep approval data structured too. Show a normalized host, method, path, named header categories, and a clear indication when the action adds a credential. Reject malformed input before the approval card appears. Asking a person to approve an invalid request moves a parser bug into a user interface.

Sallyport's vault gate, session authorization, and per-call key setting provide different moments for human control. The HTTP action boundary still needs a strict renderer because the approval decision should cover a well-formed action, not a string whose meaning changes after the click.

The first practical change is not a sweeping rewrite. Find every path that accepts agent-controlled destination text or custom headers, add an unconditional CRLF rejection before rendering, and write a transport test that proves rejected input sends nothing. Then remove the convenience code that reconstructs HTTP with strings. That is where this defect keeps coming back.
