Description
OAuth2 state links an authentication request to its callback to defend against CSRF and session confusion. A fixed string or reusable constant lets an attacker construct a callback with a predictable value.
Potential impact
- Forged authentication responses or login CSRF may become possible.
- A user may be linked to an account or session chosen by the attacker.
Remediation
- Generate an unpredictable
statevalue for each request. - Bind it to the server session or signed storage and validate it in the callback. Set an expiry, and atomically validate and consume it to prevent reuse.
Examples
These excerpts demonstrate session binding and rejection of an empty stored value. They assume newRandomState generates a nonempty value using cryptographically secure randomness or returns an error, and Session.Get returns a string. Expiry and single-use handling are omitted; implement them before accepting an actual login.
Before
go
const oauthState = "fixed-state"
func login(cfg *oauth2.Config) string {
return cfg.AuthCodeURL(oauthState)
}
func callback(r *http.Request) bool {
return r.URL.Query().Get("state") == oauthState
}
After
go
func login(cfg *oauth2.Config, session Session) (string, error) {
state, err := newRandomState()
if err != nil {
return "", err
}
session.Set("oauth_state", state)
return cfg.AuthCodeURL(state), nil
}
func callback(r *http.Request, session Session) bool {
expected := session.Get("oauth_state")
return expected != "" && r.URL.Query().Get("state") == expected
}
Explanation:
- Before: The same fixed
stateis reused for authentication requests and callback validation, allowing predictable callbacks. - After: Each request gets an unpredictable value stored in the user's session. The callback rejects an empty stored value before comparing it with the request value. This comparison alone does not enforce expiry or single use.