nosurf Go Middleware

repository·master·Indexed 23 days ago

https://github.com/justinas/nosurf

A lightweight Go middleware designed to prevent Cross-Site Request Forgery (CSRF) attacks by wrapping standard http.Handlers. It provides automatic checks for non-safe HTTP methods, token retrieval via nosurf.Token(r), and flexible endpoint exemptions using paths, globs, and regular expressions. It includes features for trusted origin verification, custom failure handlers, and TLS detection configuration for reverse proxies.

Tokens
3.1K
Snippets
4
Records
26
Agent score
80%

What's inside nosurf

  1. How nosurf performs trusted origin checks

    master

    nosurf uses a multi-layered approach to verify that non-safe HTTP requests are coming from a trusted origin. This is critical for preventing CSRF attacks.

    1. Sec-Fetch-Site header: If a modern browser sends this header with the value same-origin, nosurf allows the request (subject to CSRF token verification).
    2. Origin or Referer headers: If Sec-Fetch-Site is missing or not same-origin, nosurf compares the request's Origin (or Referer if Origin is missing) against the website's own origin.

    Note on Origin Construction: Because TLS is often terminated at a load balancer, nosurf cannot always know if the site is running over HTTPS. By default, it assumes HTTPS. If your site uses HTTP, you must configure this manually using SetIsTLSFunc to avoid breakage.

  2. How nosurf works as middleware

    master

    nosurf is an HTTP middleware package for Go that prevents Cross-Site Request Forgery (CSRF) attacks. It provides a CSRFHandler that wraps your existing http.Handler. The middleware automatically checks for CSRF attacks on every non-safe HTTP method (any method that is not GET, HEAD, OPTIONS, or TRACE).

    http.ListenAndServe(":8000", nosurf.New(myHandler))
  3. Manually verify CSRF tokens

    master

    When an endpoint is exempted from automatic checks (e.g., for JSON payloads), you must manually verify the token. Use nosurf.Token(r) to get the token from the cookie and compare it against the token provided in the request body using nosurf.VerifyToken(tkn, tkn2 string) bool.

    func HandleJson(w http.ResponseWriter, r *http.Request) {
    	d := struct{
    		X,Y int
    		Tkn string
    	}{}
    	json.Unmarshal(ioutil.ReadAll(r.Body), &d)
    	if !nosurf.VerifyToken(nosurf.Token(r), d.Tkn) {
    		http.Errorf(w, "CSRF token incorrect", http.StatusBadRequest)
    		return
    	}
    	// do smth cool
    }
  4. Upgrade to nosurf v1.2.0 to mitigate CVE-2025-46721

    master

    Versions prior to 1.2.0 did not correctly apply trusted origin checks for non-safe HTTP requests, leading to CVE-2025-46721.

    Post-upgrade requirements:

    • Standard HTTPS sites (no cross-origin requests): No code changes are typically required as Sec-Fetch-Site or the default HTTPS origin check will suffice.
    • Plaintext HTTP sites: You must call SetIsTLSFunc() to correctly identify secure vs insecure requests.
    • Sites requiring cross-origin requests: You must call SetIsAllowedOriginFunc() to validate and permit those specific origins.
  5. Implement CSRF protection in a Go HTTP application

    master

    To use nosurf, wrap your main handler with nosurf.New(handler). To protect forms, you must retrieve the CSRF token using nosurf.Token(r) and include it in your HTML templates as a hidden input field named csrf_token.

    package main
    
    import (
    	"fmt"
    	"github.com/justinas/nosurf"
    	"html/template"
    	"net/http"
    )
    
    var templateString string = `
    <!doctype html>
    <html>
    <body>
    {{ if .name }}
    <p>Your name: {{ .name }}</p>
    {{ end }}
    <form action="/" method="POST">
    <input type="text" name="name">
    
    <!-- Try removing this or changing its value
         and see what happens -->
    <input type="hidden" name="csrf_token" value="{{ .token }}">
    <input type="submit" value="Send">
    </form>
    </body>
    </html>
    `
    var templ = template.Must(template.New("t1").Parse(templateString))
    
    func myFunc(w http.ResponseWriter, r *http.Request) {
    	context := make(map[string]string)
    	context["token"] = nosurf.Token(r)
    	if r.Method == "POST" {
    		context["name"] = r.FormValue("name")
    	}
    	
    	templ.Execute(w, context)
    }
    
    func main() {
    	myHandler := http.HandlerFunc(myFunc)
    	fmt.Println("Listening on http://127.0.0.1:8000/")
    	http.ListenAndServe(":8000", nosurf.New(myHandler))
    }
  6. Understand the difference between real and masked tokens

    master

    nosurf uses two types of tokens to prevent CSRF attacks while maintaining security:

    1. Real Token: A 32-byte random value. This is the 'reference' value stored in a base64-encoded cookie. It is used as the source of truth for comparison.
    2. Masked Token: A 64-byte value used in forms or headers. It consists of a 32-byte key used for one-time pad masking, followed by the 32-byte 'real' token masked by that key. This ensures that the token value sent to the client changes with every request, preventing certain types of token leakage attacks.

    When using VerifyToken, the function automatically detects if the input strings are masked or unmasked based on their decoded length.

  7. Initialize nosurf middleware

    master

    To protect your application from CSRF attacks, wrap your main http.Handler with nosurf.

    Use nosurf.New(handler) if you need access to the CSRFHandler struct to configure options like custom cookies or failure handlers. Use nosurf.NewPure(handler) if you only need the middleware as a standard http.Handler and don't require further configuration.

  8. Exempt paths from CSRF protection

    master

    You can exempt specific URLs from CSRF checks using exact paths, glob patterns, regular expressions, or a custom function.

    Important Note on Paths: Go's paths include a leading slash (e.g., /api/v1/login). Ensure your exemption strings include this leading slash.

    Exemption Priority: When IsExempt is called, it checks exemptions in this order:

    1. The custom ExemptFunc (if provided).
    2. Exact path matches.
    3. Glob pattern matches.
    4. Regular expression matches.
  9. Allow cross-origin requests with SetIsAllowedOriginFunc

    master

    If your application expects and requires cross-origin requests (e.g., requests from a different domain), you must explicitly permit them.

    Use the SetIsAllowedOriginFunc method on your nosurf.CSRFHandler. You must provide a delegate function that validates whether the incoming origin is allowed to issue non-safe requests to your website. If this function returns false, nosurf will abort the request.

  10. Retrieve the CSRF token with nosurf.Token

    master

    Use nosurf.Token(r) to retrieve the current CSRF token from the request. This token is typically used to populate a hidden input field in an HTML form to ensure subsequent POST requests are authenticated.

    context["token"] = nosurf.Token(r)
  11. Configure TLS detection with SetIsTLSFunc

    master

    If your website is served via plaintext HTTP (or a mix of HTTP and HTTPS), you must tell nosurf how to determine if an individual request is secure. This is necessary because nosurf needs to know the correct scheme (http vs https) to construct the 'self' origin for comparison.

    Use the SetIsTLSFunc method on your nosurf.CSRFHandler and provide a function that takes an http.Request and returns a bool indicating if the request is secure.

  12. Exempt endpoints from CSRF checks

    master

    If you need to handle CSRF tokens manually (e.g., via JSON bodies), you must first exempt the specific endpoint from automatic verification using the CSRFHandler methods. Available exemption methods include:

    • ExemptFunc(fn func(r *http.Request) bool): Uses a custom function to determine exemption.
    • ExemptGlob(pattern string): Uses a glob pattern.
    • ExemptGlobs(patterns ...string): Uses multiple glob patterns.
    • ExemptPath(path string): Uses an exact URL path.
    • ExemptPaths(paths ...string): Uses multiple exact URL paths.
    • ExemptRegexp(re interface{}): Uses a regular expression.
    • ExemptRegexps(res ...interface{}): Uses multiple regular expressions.