Help us shape the future of Go! Take the 2026 Go Developer Survey

csrf

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 7 Imported by: 0

README

CSRF

Cookie-authenticated routes need protection against requests sent by other sites. arkoren.dev/core/http/middleware/csrf provides that protection with Go's http.CrossOriginProtection. On an existing r router, allow a trusted partner origin in addition to same-origin requests:

r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.TrustedOrigins("https://partner.example")
}))

Without a callback, Middleware() adds no trusted cross-origin exceptions. GET, HEAD, and OPTIONS always pass and must not change state. Requests without Sec-Fetch-Site or Origin also pass. This is not authentication or authorization.

Use csrf.Builder.Protection to supply your own http.CrossOriginProtection (for bypass patterns); csrf.Builder.TrustedOrigins origins are added to it. csrf.Builder.ErrorResponse replaces the default csrf.ErrBlocked problem.

Builder.Events reports each blocked request synchronously. Its concrete path, Origin header, and native error can contain sensitive or client-controlled data; review those values before forwarding notifications outside the process.

See the CSRF Protection guide for trusted origins, bypass policies, and browser-security limits.

Documentation

Overview

Package csrf provides Cross-Site Request Forgery protection middleware. It uses net/http.CrossOriginProtection with no trusted cross-origin exceptions unless configured. Safe methods GET, HEAD, and OPTIONS always pass and must not change state. Requests without Sec-Fetch-Site or Origin headers also pass; cross-origin protection is not authentication or authorization.

Install the same-origin policy before registering routes:

routes := router.New()
routes.Use(csrf.Middleware())

With Builder.Events, every blocked request publishes Blocked ("http.csrf.blocked") with the method, path, Origin header, and native error. Listeners run synchronously; review client-controlled and potentially sensitive diagnostic values before forwarding them outside the process.

Index

Examples

Constants

This section is empty.

Variables

View Source
var Blocked = event.NewTopic[BlockedEvent]("http.csrf.blocked")

Blocked is published when Middleware rejects a cross-origin request. Supply its bus through Builder.Events; omitted or nil buses publish nothing. Listeners run synchronously and cannot change the rejection outcome.

Example:

events := event.New()
unsubscribe, err := events.Listen(csrf.Blocked,
	func(_ context.Context, notification csrf.BlockedEvent) error {
		fmt.Println(notification.Method, notification.Path)

		return nil
	})
if err != nil {
	return err
}
defer unsubscribe()
View Source
var ErrBlocked = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/middleware/csrf:blocked")
	details.Title("Cross-Origin Request Blocked")
	details.Detail("The request was rejected because its origin or fetch context did not meet security requirements.")
	details.Status(http.StatusForbidden)
})

ErrBlocked is the default error returned when a CSRF attack is detected. It contains a structured problem response with appropriate HTTP status and details that can be safely returned to clients without exposing security implementation details.

Example:

failure := csrf.ErrBlocked.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()

Functions

func Middleware

func Middleware(configure ...BuilderFunc) router.Middleware

Middleware returns a middleware that protects against Cross-Site Request Forgery attacks using Go's built-in http.CrossOriginProtection. It panics when a trusted origin is malformed. Safe methods (GET, HEAD, OPTIONS) are never checked, so they must not change state. With Builder.Events every blocked request publishes Blocked. Requests without Sec-Fetch-Site or Origin headers are allowed; this policy is not authentication. No trusted cross-origin exceptions are added by default.

Example:

routes := router.New()
routes.Use(csrf.Middleware())

Types

type BlockedEvent

type BlockedEvent struct {
	// Method is the blocked request's method, without normalization.
	//
	// Example:
	//
	//	notification := csrf.BlockedEvent{Method: http.MethodPost}
	//	fmt.Println(notification.Method)
	Method string

	// Path is the request path without its query, not a route template. It can
	// still contain personal identifiers.
	//
	// Example:
	//
	//	notification := csrf.BlockedEvent{Path: "/settings"}
	//	fmt.Println(notification.Path)
	Path string

	// Origin is the untrusted Origin header; empty when the request had none.
	// It is reported for diagnosis, not as an authenticated identity.
	//
	// Example:
	//
	//	notification := csrf.BlockedEvent{Origin: "https://other.example"}
	//	fmt.Println(notification.Origin)
	Origin string

	// Err is the reason [net/http.CrossOriginProtection] gave. Its text is native
	// diagnostic information, not an application-facing response body.
	//
	// Example:
	//
	//	notification := csrf.BlockedEvent{Err: errors.New("cross-origin request rejected")}
	//	fmt.Println(notification.Err != nil)
	Err error
}

BlockedEvent reports a request rejected by the configured native cross-origin policy. Paths, Origin, and the native error can contain untrusted or sensitive data; review them before forwarding a notification outside the process.

Example:

notification := csrf.BlockedEvent{
	Method: http.MethodPost,
	Path:   "/settings",
	Origin: "https://other.example",
}
fmt.Println(notification.Method, notification.Path)

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder configures Middleware before publication. Callbacks run directly once, synchronously in order on one fresh draft; nil entries panic. Retaining the draft cannot change published settings. Methods return the same draft.

Example:

r := router.New()
r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.TrustedOrigins("https://app.example.com")
}))

func (*Builder) ErrorResponse

func (protection *Builder) ErrorResponse(response problem.Details) *Builder

ErrorResponse is the problem returned for a blocked request. Zero uses ErrBlocked.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.ErrorResponse(problem.Forbidden())
}))

func (*Builder) Events

func (protection *Builder) Events(bus *event.Bus) *Builder

Events receives Blocked. Nil, the default, publishes nothing.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.Events(event.New())
}))

func (*Builder) Protection

func (protection *Builder) Protection(engine *http.CrossOriginProtection) *Builder

Protection is the engine that checks requests. Zero builds a new http.CrossOriginProtection. A provided value is mutated to add TrustedOrigins.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.Protection(http.NewCrossOriginProtection())
}))

func (*Builder) TrustedOrigins

func (protection *Builder) TrustedOrigins(origins ...string) *Builder

TrustedOrigins is the list of trusted origins allowed to make cross-origin requests. They are added to Protection when both are set.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(csrf.Middleware(func(protection *csrf.Builder) {
	protection.TrustedOrigins("https://app.example.com")
}))

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits the construction draft shared by Middleware's callbacks. Callbacks run synchronously in order; a supplied nil callback panics, while expanding a nil or empty callback slice makes no edits. Trusted-origin lists are copied; a supplied native protection engine remains shared.

Example:

configure := csrf.BuilderFunc(func(protection *csrf.Builder) {
	protection.TrustedOrigins("https://partner.example")
})
routes := router.New()
routes.Use(csrf.Middleware(configure))
Example

ExampleBuilderFunc deliberately adds a trusted partner origin while keeping the remaining native cross-origin policy unchanged.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"arkoren.dev/core/http/middleware/csrf"
	"arkoren.dev/core/http/router"
)

func main() {
	configure := csrf.BuilderFunc(func(protection *csrf.Builder) {
		protection.TrustedOrigins("https://partner.example")
	})

	routes := router.New()
	routes.Use(csrf.Middleware(configure))
	routes.Post("/", func(http.ResponseWriter, *http.Request) error {
		return nil
	})

	request := httptest.NewRequest(http.MethodPost, "https://service.example/", nil)
	request.Header.Set("Origin", "https://partner.example")
	request.Header.Set("Sec-Fetch-Site", "cross-site")

	recorder := httptest.NewRecorder()

	routes.ServeHTTP(recorder, request)
	fmt.Println(recorder.Code)
}
Output:
204

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL