Frontend Labs
⋯

Day 62 — net/http Server Configuration

Learning Objectives

  • Configure all four http.Server timeouts — each prevents a different attack
  • Use ConnState callback to track connection lifecycle
  • Understand why default http.Server is insecure for production

Key Concepts

http.Server timeout fields and what they prevent:

  ReadHeaderTimeout: 5s  — Slowloris: attacker sends headers 1 byte/sec
  ReadTimeout:      30s  — Slow body: attacker sends body 1 byte/sec
  WriteTimeout:     30s  — Slow read: attacker reads response 1 byte/sec
  IdleTimeout:     120s  — Connection leeching: attacker holds keep-alive open
  MaxHeaderBytes:  64KB  — Header bomb: attacker sends huge headers

Without these:
  Slowloris + 10,000 connections = server out of file descriptors
  Default http.Server has NO timeouts — use ONLY with a reverse proxy in front

Exercise: Hardened HTTP Server

package main

import (
    "fmt"
    "net/http"
    "sync/atomic"
    "time"
)

type ConnStats struct {
    New     atomic.Int64
    Active  atomic.Int64
    Idle    atomic.Int64
    Closed  atomic.Int64
}

func (cs *ConnStats) Handler(conn net.Conn, state http.ConnState) {
    switch state {
    case http.StateNew:
        cs.New.Add(1)
        cs.Active.Add(1)
    case http.StateActive:
        cs.Idle.Add(-1)
    case http.StateIdle:
        cs.Active.Add(-1)
        cs.Idle.Add(1)
    case http.StateClosed, http.StateHijacked:
        cs.Active.Add(-1)
        cs.Idle.Add(-1)
        cs.Closed.Add(1)
    }
}

func main() {
    stats := &ConnStats{}
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "OK\n")
    })
    mux.HandleFunc("/conn-stats", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "new=%d active=%d idle=%d closed=%d\n",
            stats.New.Load(), stats.Active.Load(), stats.Idle.Load(), stats.Closed.Load())
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,

        // SECURITY: all timeouts must be set in production
        ReadHeaderTimeout: 5 * time.Second,   // Slowloris defense
        ReadTimeout:       30 * time.Second,  // slow body defense
        WriteTimeout:      30 * time.Second,  // slow client defense
        IdleTimeout:       120 * time.Second, // keep-alive limit
        MaxHeaderBytes:    64 * 1024,         // 64KB header limit

        // Connection lifecycle monitoring
        ConnState: stats.Handler,
    }

    fmt.Println("Hardened HTTP server on :8080")
    fmt.Println("Test Slowloris: pip install slowloris && slowloris -p 8080 localhost")
    fmt.Println("  With ReadHeaderTimeout=5s: server stays alive")
    fmt.Println("  Without timeout: server dies in ~30s")

    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        fmt.Println("ERROR:", err)
    }
}

Real-World Context

The default http.ListenAndServe(":8080", handler) creates a server with NO timeouts. Behind a properly configured nginx or ALB, this is fine. Directly on the internet: Slowloris will take it down in minutes. PCI DSS and SOC 2 scans specifically check for missing timeout configurations.


Generated from week9.md · 8 Oct 2026 16:36