fasthttp icon indicating copy to clipboard operation
fasthttp copied to clipboard

FastHTTP incorrectly handles obs-fold on the first line

Open kenballus opened this issue 9 months ago • 1 comments

When FastHTTP receives a request in which the first header line begins with spaces, it allows the spaces to persist into the header name. This is invalid, because spaces are not permitted in header names.

This can be confirmed by

  1. running a FastHTTP server that echoes header names (like this one),
  2. sending it a request with a header name prefixed with spaces, and extracting the echoed header name:
printf 'GET / HTTP/1.1\r\n  Test: whatever\r\n\r\n' \
  | nc localhost 80 \
  | grep "headers" \
  | jq '.["headers"][0][0]' \
  | xargs echo \
  | base64 -d \
  | xxd
00000000: 2020 5465 7374                             Test

Note that the spaces are still there in the header name.

The correct behavior in this scenario is to reject the request with a 400.

kenballus avatar Mar 09 '25 23:03 kenballus


package main

import (
	"encoding/json"
	"fmt"
	"log"
	"regexp"

	"github.com/valyala/fasthttp"
)

// validateHeaderName checks if a header name is valid per RFC 7230.
// Returns true if valid, false if invalid (e.g., contains spaces or invalid chars).
func validateHeaderName(name []byte) bool {
	// RFC 7230 token: ALPHA / DIGIT / ! # $ % & ' * + - . ^ _ ` | ~
	// We can use a regex or character check. Regex is simpler for clarity.
	// Invalid if contains spaces, tabs, or non-token chars.
	validHeaderName := regexp.MustCompile(`^[!#$%&'*+\-.^_\`|~0-9A-Za-z]+$`)
	return validHeaderName.Match(name)
}

// requestHandler validates headers and processes the request.
func requestHandler(ctx *fasthttp.RequestCtx) {
	// Validate all header names
	ctx.Request.Header.VisitAll(func(key, value []byte) {
		if !validateHeaderName(key) {
			ctx.SetStatusCode(fasthttp.StatusBadRequest)
			ctx.SetContentType("text/plain")
			fmt.Fprintf(ctx, "Invalid header name: %q contains spaces or invalid characters", key)
			return
		}
	})

	// If we reached here, headers are valid (or response was already set)
	if ctx.Response.StatusCode() == fasthttp.StatusBadRequest {
		return
	}

	// Echo headers as JSON for valid requests (mimicking user's test case)
	headers := make([][]string, 0)
	ctx.Request.Header.VisitAll(func(key, value []byte) {
		headers = append(headers, []string{string(key), string(value)})
	})

	response := map[string][][]string{
		"headers": headers,
	}

	ctx.SetContentType("application/json")
	ctx.SetStatusCode(fasthttp.StatusOK)
	if err := json.NewEncoder(ctx).Encode(response); err != nil {
		ctx.SetStatusCode(fasthttp.StatusInternalServerError)
		fmt.Fprintf(ctx, "Failed to encode response: %v", err)
		return
	}
}

func main() {
	// Create FastHTTP server
	server := &fasthttp.Server{
		Handler: requestHandler,
		Name:    "TestServer",
	}

	// Start server on port 80
	if err := server.ListenAndServe(":80"); err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
}

ljluestc avatar Jun 01 '25 20:06 ljluestc

Of course you could always glue the validation on top. The issue is that this is not handled automatically, as it is in nearly all other HTTP libraries.

kenballus avatar Jun 24 '25 16:06 kenballus