Back to Articles

rawhttp: Crafting Malformed HTTP Requests in Go for Security Testing

[ View on GitHub ]

rawhttp: Crafting Malformed HTTP Requests in Go for Security Testing

Hook

Go's net/http package will reject your HTTP request if you try to send a method with lowercase letters—but what if you need to test how a server handles that exact scenario?

Context

When you're building a typical web application, Go's net/http package is a blessing. It validates your requests, manages connections efficiently, and prevents you from shooting yourself in the foot. But if you're a security researcher, penetration tester, or protocol developer, that same protective layer becomes a cage. You can't test how a server handles double Content-Length headers, you can't send requests with custom line endings to probe for HTTP request smuggling vulnerabilities, and you certainly can't craft intentionally malformed requests to validate server-side input handling.

This is where rawhttp enters the picture. Created by Tom Hudson (tomnomnom), a prominent figure in the bug bounty and security research community, rawhttp is a deliberately minimal Go library that gives you direct control over every byte that goes over the wire. It's not trying to be a better HTTP client—it's trying to be a more honest one. Instead of abstracting away the HTTP protocol, it exposes it completely, validation and all safety rails removed. Think of it as the difference between driving an automatic car with lane assist versus a manual transmission race car: one keeps you safe on your commute, the other gives you control when you need to push boundaries.

Technical Insight

rawhttp Library

Configure Fields

Call Do

Connect to Host:Port

Write Raw HTTP

Send Over

Transmit

HTTP Response

Read Response

User Code

Req Struct

Method, Path, Proto

Headers, Body

Serialize Method

Build Raw HTTP

TCP/TLS Connection

net.Dial / tls.Dial

Raw Bytes

Over Network

Target Server

System architecture — auto-generated

The architecture of rawhttp is refreshingly simple: it's essentially a struct that represents an HTTP request with all fields exposed as public properties, plus methods to send that request over a TCP or TLS connection. Unlike net/http's Request type, which encapsulates behavior and enforces standards compliance, rawhttp's Req struct is deliberately dumb—it serializes whatever you give it.

Here's a basic example that demonstrates the core difference. With net/http, you might write:

req, _ := http.NewRequest("GET", "https://example.com/", nil)
resp, _ := http.DefaultClient.Do(req)

With rawhttp, the same request looks like this:

import "github.com/tomnomnom/rawhttp"

req := rawhttp.Req{
    Method: "GET",
    Path: "/",
    Proto: "HTTP/1.1",
    Headers: []string{
        "Host: example.com",
        "User-Agent: rawhttp",
    },
}

resp, err := req.Do("example.com:443", true) // true enables TLS

The difference becomes apparent when you want to do something non-standard. Want to test how a server handles a lowercase HTTP method? With net/http, you're out of luck—it will uppercase it for you. With rawhttp:

req := rawhttp.Req{
    Method: "get", // lowercase, potentially malformed
    Path: "/../../../etc/passwd", // unencoded path traversal attempt
    Proto: "HTTP/1.1",
    Headers: []string{
        "Host: example.com",
        "Content-Length: 5",
        "Content-Length: 10", // duplicate header for smuggling tests
    },
    Body: "hello",
}

This request would never make it past net/http's validation, but rawhttp will happily send it. The library provides a few convenience methods like AutoSetHost() and AutoSetContentLength() that can populate standard headers for you, but they're entirely optional—you're in control.

The actual HTTP request construction happens in the String() method, which serializes the Req struct into the wire format. This is where you can also customize line endings. By default, rawhttp uses "\r\n" (CRLF) as specified by the HTTP standard, but you can override this with the EOL field:

req := rawhttp.Req{
    Method: "GET",
    Path: "/",
    Proto: "HTTP/1.1",
    EOL: "\n", // Use LF instead of CRLF
    Headers: []string{"Host: example.com"},
}

This capability is particularly useful for testing HTTP request smuggling vulnerabilities, where differences in how servers parse line endings can lead to security issues. Some backend servers might accept LF-only line endings while frontend proxies expect CRLF, creating desynchronization opportunities.

The response handling is equally minimal. The Do() method returns a rawhttp.Resp struct that contains the raw HTTP response with the status line, headers, and body exposed as public fields. There's no automatic decompression, no cookie jar, no redirect following—just the bytes that came back over the wire. For security testing, this is exactly what you want: no magic, no surprises, just direct access to the server's actual response.

Gotcha

The biggest gotcha with rawhttp is right there in the README: the API is explicitly marked as unfixed and subject to breaking changes. This isn't a library you should embed in production code expecting long-term stability. Tom Hudson built this as a tool for his own security research, and while he's shared it publicly, there's no guarantee of maintenance or backward compatibility.

More fundamentally, the lack of validation is a double-edged sword. While it enables the security testing use cases the library was designed for, it also means you can easily create completely broken requests without any warning. There's no checking that your headers are properly formatted, that your Content-Length matches your body size, or that your method is a valid HTTP verb. You can send a request with the method "BANANA" and rawhttp will dutifully transmit it. For experienced developers who understand the HTTP protocol deeply, this is fine—you know what you're doing. But for those learning or casually experimenting, it's easy to spend time debugging why your request isn't working only to discover you forgot to include a Host header or miscalculated the Content-Length. The library also doesn't handle connection pooling, keep-alive, or any of the performance optimizations that modern HTTP clients provide. You're managing raw TCP/TLS connections, which means each request potentially opens a new connection. For fuzzing or security testing where you're sending hundreds or thousands of requests, you'll need to implement your own connection management or accept the performance penalty.

Verdict

Use if: You're doing security research, penetration testing, or bug bounty hunting where you need to craft non-standard HTTP requests that standard libraries reject. Use if you're testing HTTP request smuggling vulnerabilities, fuzzing HTTP parsers, or validating edge case handling in web servers. Use if you're comfortable with the HTTP protocol at the wire level and understand the implications of bypassing standard validation. Skip if: You're building production application code—net/http or higher-level clients like resty are better choices. Skip if you need features like connection pooling, automatic retries, cookie management, or redirect following. Skip if you're not deeply familiar with the HTTP specification and might accidentally create malformed requests. Skip if you need API stability guarantees or long-term support. This is a specialized tool for a specific audience: security researchers who need a scalpel, not a Swiss Army knife.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/tomnomnom-rawhttp.svg)](https://starlog.is/api/badge-click/developer-tools/tomnomnom-rawhttp)