Created by Gustaf Neumann, last modified by Gustaf Neumann 23 Feb 2026, at 06:28 PM
This recipe shows how to enforce POST for state-changing endpoints and how it fits with CSRF protection in OpenACS.
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:
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=Lax cookies.
-
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
Created by Gustaf Neumann, last modified by Gustaf Neumann 23 Feb 2026, at 05:08 PM
The newest versions of OpenACS (head) and NaviServer (5.1) can now process JSON request bodies in the same convenient way as classic HTML form submissions. When a request is sent with a JSON media type (including structured suffix types such as application/*+json), NaviServer parses the JSON payload automatically and flattens it into the connection form set. This enables using ad_page_contract for validation and makes JSON requests first-class citizens in existing OpenACS applications.
On validation failures, OpenACS can return a standards-based error response using RFC 9457 (application/problem+json) and can include JSON Pointer references (RFC 6901) to pinpoint the offending fields.
Overview
- Automatic JSON parsing: JSON request bodies are parsed and exposed through
ns_getform/ns_conn form as an ns_set.
- Typed values: The flattened representation includes
.type sidecar keys (e.g., user/id.type, user/flags/admin.type) to preserve JSON typing.
- JSON Pointer addressing (RFC 6901): When reporting errors (and when navigating typed JSON structures via the
ns_json triples ensemble), JSON Pointer provides an interoperable path syntax such as /user/flags/admin.
- Problem Details errors (RFC 9457): Contract violations for JSON requests can return
422 Unprocessable Content with application/problem+json.
Sending JSON Requests
Clients should send JSON with an appropriate Content-Type header:
Content-Type: application/json
Structured syntax suffixes are supported as well:
Content-Type: application/vnd.api+json
Using ad_page_contract with JSON
In OpenACS, you can validate JSON-derived values using ad_page_contract. Because the JSON body is flattened into an ns_set, contract parameter names can refer to nested JSON fields using a slash-separated naming convention.
Example OpenACS page
ad_page_contract {
test_page
} {
{user/id:naturalnum 0}
{user/flags/admin:boolean}
}
ns_return 200 application/json [subst {{"result":"OK", "user_id":${user/id}}}]
The contract declares two inputs:
user/id must be a natural number (integer >= 0) and defaults to 0
user/flags/admin must be a boolean
Example Request Payload
Submitted JSON data:
{"user": {
"id":-7,
"name":"Alice",
"flags":{
"admin":"xxx",
"active":true
}}
}
Validation Failure Response (RFC 9457 + RFC 6901)
When the request body was parsed as JSON and validation fails, OpenACS can return a Problem Details document (application/problem+json) with status 422. Each error includes a JSON Pointer fragment (#/...) referencing the offending field in the request document.
{
"type": "https://openacs.org/validation-error",
"title": "We had a problem with your input:",
"status": 422,
"errors": [
{
"detail": "user/id is not a natural number, that is an integer greater than or equal to 0.",
"pointer": "#/user/id"
},
{
"detail": "user/flags/admin does not appear to be a Boolean value.",
"pointer": "#/user/flags/admin"
}
]
}
Notes
- The
type field is a stable identifier (a URI) for the class of problem. It may be used by clients for automatic handling and may also point to documentation.
- The
title and detail fields are intended for humans and may be localized. Clients should not rely on localized text for program logic.
- The
pointer values use JSON Pointer fragment identifiers (#/...), consistent with the Problem Details ecosystem. The corresponding JSON Pointer path without fragment prefix would be /user/id, /user/flags/admin, etc.
Optional: Typed JSON Navigation with ns_json triples
For applications that need to navigate and update JSON while preserving JSON types (numbers, booleans, null, arrays, objects), NaviServer provides the ns_json triples ensemble. It supports addressing via Tcl list paths or JSON Pointer (RFC 6901) and can generate JSON output directly from triples without lossy conversions. Alternatively of using the ns_set provided by ns_getform, one can get the body of the request and process the included JSON, extract parts of it, etc.
Typical flow:
set t [ns_json parse -output triples $json]
ns_json triples getvalue -pointer /user/flags $t
set t2 [ns_json triples setvalue -pointer /user/flags/admin -type boolean $t true]
ns_json triples getvalue -pretty -pointer /user/flags $t2
See Also