7 min read

Can HTTP request injection start with a line break?

HTTP request injection can begin with a hidden line break. Learn how to reject CRLF in destinations and headers before credentialed requests render.

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:

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:

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:

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:

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

So must this one:

{
  "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

Approve well-formed HTTP actions
Reject malformed agent actions before Sallyport performs the credentialed HTTP call.

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:

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://[email protected]/. 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

Keep evidence for every call
The Activity journal records individual calls from the same encrypted, hash-chained audit log.

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

Verify the action trail offline
Verify Sallyport's encrypted audit chain offline with sp audit verify, without a vault key.

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:

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.

FAQ

Should I reject CRLF in HTTP header values?

Reject both characters anywhere they can enter request syntax: destination strings, header names, header values, method overrides, cookie fragments, and proxy settings. Treat percent-decoded data the same way. Validation must happen before interpolation, parsing, or a library call that might normalize the input.

Can a safe HTTP library prevent request smuggling?

A normal HTTP client library helps, but it does not remove the need for boundary validation. Your gateway may decode, combine, or format agent input before it reaches the library, and some inputs affect a different parser first. Rejecting line breaks at the boundary makes the intended contract explicit.

Why are carriage returns dangerous in API requests?

No. Newlines separate HTTP message syntax, so allowing them inside a destination or header field can turn data into another field or another request component. The vulnerable code may still send a syntactically valid request, which is why this bug can hide in ordinary testing.

Does percent-encoded CRLF bypass validation?

The parser should decode once, then validate the decoded value. If it accepts a percent-encoded newline and a later layer decodes it, the check happened too early. Keep decoded and rendered request values separate so later code does not decode them again.

How should I validate custom header names?

Header names should use a small allowlist such as letters, digits, and hyphen, then reject anything else. This prevents line breaks, colon insertion, whitespace tricks, and names that different components interpret differently. Do not accept arbitrary agent-provided header names merely because the value is checked.

Is URL parsing enough to secure an agent destination?

No. A URL parser can tell you where the host and path boundaries are, but it cannot decide whether a destination is permitted. Parse first, then apply your own rules for scheme, host, port, user info, fragments, and decoded control characters.

What should an action gateway do when it sees a newline?

Reject it before building any request object. Do not trim it, replace the line break with a space, or silently remove it, because those repairs hide the original action from logs and can change the meaning of signed or authenticated input. Return an error that identifies the field without echoing a secret.

Should validation happen before or after URL decoding?

Check the raw input, then the once-decoded representation, then the final serialized bytes in tests. The first check catches obvious bad input; the decoded check catches encoding bypasses; the serialization test catches unsafe formatting code. One check alone leaves room for a later transformation to change the value.

Can credential injection cause request injection?

Even if the credential is added by trusted code, the surrounding request may still be controlled by an agent. A hostile header value can place a second header next to the injected credential, while a hostile destination can redirect where the credentialed request goes. Protecting the secret and protecting the request shape are separate jobs.

What test cases catch HTTP header injection?

Test every accepted input position with literal carriage return, literal newline, CRLF, percent-encoded forms, double-encoded forms, leading whitespace, and a colon in a header name. Assert that the gateway rejects each case before network I/O. Then inspect a captured request for approved cases and verify that it contains exactly the fields you intended.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov