Enforcing POST for state-changing requests (require_post)
Created by Gustaf Neumann, last modified by Gustaf Neumann 06:54 PM, Friday
Motivation
State-changing operations (e.g., create/update/delete actions, membership or role management, administrative tasks) should not be reachable via HTTP GET requests.
GET requests are easy to trigger unintentionally or via social engineering (for example by clicking a link in an email). In modern browsers, session cookies with SameSite=Lax are still sent on cross-site top-level GET navigations, which makes such endpoints vulnerable to CSRF-style attacks if no additional protection is in place.
To reduce this attack surface, OpenACS applications can explicitly require HTTP POST for state-changing requests.
The ::template::require_post validator
The procedure ::template::require_post enforces that the current request uses the HTTP POST method. If the request is issued using any other method (typically GET), it returns a 405 Method Not Allowed response and aborts request processing.
This validator is intended to be used in the -validate blocks of ad_page_contract and ad_form.
Example A: Using require_post in ad_page_contract
ad_page_contract {
...
} -query {
email ...
} -validate {
method { require_post }
csrf { csrf::validate }
}
# Page implementation follows
In this example:
-
require_postensures that the request cannot be triggered via GET. -
csrf::validateperforms explicit CSRF token validation.
Placing the method check early in the validation phase ensures that invalid requests are rejected before any state-changing logic is executed.
Example B: Using require_post in ad_form
# If the data is not submitted via a POST request, bail out hard.
# The request might originate from a forged or unintended request.
ad_form \
-name myform \
-form {
{email:text {label "E-Mail"}} ...
} -validate {
{email
{ [require_post] }
"Only POST requests are allowed"
}
} -on_submit {
...
}
This pattern attaches the POST requirement directly to the form validation phase and ensures that the form submission cannot be triggered via a simple GET request.
Caveat: POST is not sufficient by itself
Requiring POST does not replace CSRF protection.
A determined attacker can still trigger cross-site POST requests (for example via auto-submitted forms). Proper CSRF protection therefore must include explicit CSRF token validation.
Requiring POST should be understood as a defense-in-depth measure:
-
It prevents accidental or link-based triggering of state-changing actions.
-
It improves the effectiveness of
SameSite=Laxcookies. -
It reduces the risk of subtle CSRF bugs caused by overlooked GET endpoints.
For full protection, state-changing endpoints should:
-
Require POST
-
Validate a CSRF token
-
(Optionally) perform "Origin" or "Referer" checks for high-risk administrative actions
When to use this pattern
Use require_post for:
-
administrative pages (e.g., under
/admin) -
role or permission changes
-
membership management
-
any operation that modifies server-side state
Do not use require_post for:
-
read-only pages
-
bookmarkable navigation URLs
-
search, filtering, or pagination endpoints
See also
-
CSRF protection in OpenACS (cookbook)
https://openacs.org/xowiki/CSRF