From 8917c173a1ee62aa3422517b6956f3a18896349b Mon Sep 17 00:00:00 2001 From: XananasX7 Date: Tue, 16 Jun 2026 14:03:12 +0000 Subject: [PATCH 1/2] fix: skip duplicate HTTP status lines in ReadResponse Some non-compliant servers (e.g. Grandstream HT801 firmware 1.0.13.7) emit the HTTP status line more than once before the actual headers: HTTP/1.0 200 OK\r\n HTTP/1.0 200 OK\r\n <- duplicate Content-Type: text/html\r\n ... The rawhttp parser passes header lines to ReadHeader(), which splits on ':'. A duplicate status line has no ':', so parsing fails with: malformed MIME header: missing colon: "HTTP/1.0 200 OK" This propagates to Nuclei as a fatal request error even under unsafe: true, preventing matchers from ever running. Fix: after reading the first status line in ReadResponse(), peek ahead and discard any additional lines that start with 'HTTP/' before entering the header-reading loop. Adds regression tests for duplicate and triple status lines. Fixes projectdiscovery/rawhttp#541 Related: projectdiscovery/nuclei#7363 --- client/client.go | 18 ++++++++++++++++++ client/reader_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/client/client.go b/client/client.go index b822e91..1b7f903 100644 --- a/client/client.go +++ b/client/client.go @@ -124,6 +124,13 @@ func (c *client) WriteRequest(req *Request) error { return c.WriteBody(req.Body) } +// isStatusLine reports whether line looks like an HTTP status line +// (e.g. "HTTP/1.0 200 OK"). It is used to skip duplicate status lines +// emitted by some non-compliant servers. +func isStatusLine(line []byte) bool { + return len(line) >= 5 && string(line[:5]) == "HTTP/" +} + // ReadResponse unmarshalls a HTTP response. func (c *client) ReadResponse(forceReadAll bool) (*Response, error) { version, code, msg, err := c.ReadStatusLine() @@ -131,6 +138,17 @@ func (c *client) ReadResponse(forceReadAll bool) (*Response, error) { if err != nil { return nil, fmt.Errorf("ReadStatusLine: %v", err) } + // Some non-compliant servers (e.g. certain embedded devices) repeat the + // status line one or more times before the actual headers. Skip any such + // duplicate status lines so that header parsing succeeds. + for { + peeked, peekErr := c.Peek(5) + if peekErr != nil || !isStatusLine(peeked) { + break + } + // Discard the entire duplicate status line (read until newline). + _, _ = c.ReadBytes('\n') + } for { var key, value string var done bool diff --git a/client/reader_test.go b/client/reader_test.go index 77966f1..ed1dece 100644 --- a/client/reader_test.go +++ b/client/reader_test.go @@ -2,6 +2,7 @@ package client import ( "bufio" + "io" "strings" "testing" @@ -65,3 +66,30 @@ func TestReadVersion(t *testing.T) { }) } } + +// readWriterStub wraps a strings.Reader to satisfy io.ReadWriter (writes are no-ops). +type readWriterStub struct{ *strings.Reader } + +func (readWriterStub) Write(p []byte) (int, error) { return len(p), nil } + +func TestReadResponseDuplicateStatusLines(t *testing.T) { + // Some non-compliant embedded devices (e.g. Grandstream HT801) emit the + // HTTP status line twice before the headers. rawhttp must not error on this. + raw := "HTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\nhello" + c := NewClient(readWriterStub{strings.NewReader(raw)}) + resp, err := c.ReadResponse(false) + require.NoError(t, err, "ReadResponse must not fail on duplicate status lines") + require.Equal(t, 200, resp.Status.Code) + body, _ := io.ReadAll(resp.Body) + require.Equal(t, "hello", string(body)) +} + +func TestReadResponseTripleStatusLines(t *testing.T) { + // Also handle three consecutive status lines. + raw := "HTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nX-Custom: test\r\n\r\n" + c := NewClient(readWriterStub{strings.NewReader(raw)}) + resp, err := c.ReadResponse(false) + require.NoError(t, err, "ReadResponse must not fail on triple status lines") + require.Equal(t, 200, resp.Status.Code) + require.Equal(t, "test", resp.Headers[0].Value) +} From ac0c5d10229df66aaed74932caf206f9f2720921 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Tue, 16 Jun 2026 22:52:51 +0200 Subject: [PATCH 2/2] harden parser --- client/client.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 1b7f903..09a7931 100644 --- a/client/client.go +++ b/client/client.go @@ -124,11 +124,16 @@ func (c *client) WriteRequest(req *Request) error { return c.WriteBody(req.Body) } +// maxDuplicateStatusLines bounds how many repeated status lines we are willing +// to discard, so a misbehaving (or malicious) server streaming "HTTP/..." lines +// indefinitely cannot keep us looping. +const maxDuplicateStatusLines = 8 + // isStatusLine reports whether line looks like an HTTP status line // (e.g. "HTTP/1.0 200 OK"). It is used to skip duplicate status lines // emitted by some non-compliant servers. func isStatusLine(line []byte) bool { - return len(line) >= 5 && string(line[:5]) == "HTTP/" + return bytes.HasPrefix(line, []byte("HTTP/")) } // ReadResponse unmarshalls a HTTP response. @@ -141,13 +146,15 @@ func (c *client) ReadResponse(forceReadAll bool) (*Response, error) { // Some non-compliant servers (e.g. certain embedded devices) repeat the // status line one or more times before the actual headers. Skip any such // duplicate status lines so that header parsing succeeds. - for { + for i := 0; i < maxDuplicateStatusLines; i++ { peeked, peekErr := c.Peek(5) if peekErr != nil || !isStatusLine(peeked) { break } // Discard the entire duplicate status line (read until newline). - _, _ = c.ReadBytes('\n') + if _, err := c.ReadBytes('\n'); err != nil { + break + } } for { var key, value string