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 ¶
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()
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 ¶
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 ¶
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 ¶
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