Protect forms against CSRF and automated abuse
Place a tested PHP helper within a complete workflow and enforce controls before costly processing.

Different attacks need different controls
CSRF protection addresses an unwanted state-changing request that another site induces an authenticated person’s browser to make. Automated abuse presents a different problem: a program can call the intended interface itself and produce large numbers of requests or costly operations. A hidden token therefore does not establish that an enquiry is useful, authorised or affordable to process. The service needs an explicit agreement about each of those questions.
We use two original fictional workflows. An authenticated service application lets a customer change notification preferences. A separate public form accepts repair enquiries from new visitors. The account setting needs a clear relationship to the session and the permitted account action. The public enquiry needs meaningful input rules, manageable volume and a sustainable handling process. Both interfaces can be well designed while having distinct protection requirements.
Before selecting a technique, record the consequence of a malicious request. For the setting, it is an unwanted change made in a customer’s name. For the enquiry, it could be an overloaded handling queue or repeated chargeable delivery attempts. These consequences determine where processing must stop. A reassuring security symbol displayed beside the submit button cannot substitute for this analysis of the actual business action.
Name the operation being protected
Our account example changes only the preference for a weekly summary. Even this modest change belongs to the authenticated account. The server must establish the session and permission for that particular account rather than trusting a customer identifier supplied in the form. Request-forgery checks and access control remain separate decisions. Passing one must not silently be treated as satisfying the other.
For the example’s internal sequence, we distinguish authentication, request checks, volume admission, business validation and the actual update. A negative result stops the workflow before the update. This is our own planning model, not a universal ordering imposed on every application. A real implementation must also identify expensive preliminary work and operations reachable before login. Appropriate limits need to precede the work they are supposed to protect.
The public repair form does not invent a customer permission requirement: an unfamiliar person is allowed to enquire. That does not mean every syntactically valid submission must trigger unlimited downstream work. We separately define sustainable admission and the route available to a legitimate visitor after a temporary rejection. Keeping those decisions explicit avoids confusing an open service with an unbounded one.
Build a clear session-bound token contract
The PHP example generates 32 random bytes with random_bytes and encodes them as 64 lowercase hexadecimal characters. The token is retained in server-side session state and supplied in a hidden form control when the form is rendered. On submission, the implementation compares the provided value with the expected session value using hash_equals. It does not invent a character-by-character comparison routine.
The original helper checks types and the agreed format before comparing values. An array supplied where a scalar form token is expected is rejected predictably. That is a practical PHP boundary case because a direct client does not have to reproduce the structure exposed by the page. The 64-character representation is this helper’s specific contract. A mature existing implementation may use a different explicitly designed representation.
The example retains a valid token within the same session to support multiple open form views. Per-request tokens are a different possible design but require additional decisions about older tabs, back navigation and repeated attempts. The important point is deliberate lifecycle ownership. When authentication state changes, the surrounding application must renew session state and token policy as appropriate rather than treating a token as an indefinitely reusable account credential.
Check origin and browser context deliberately
The controlled browser application in this example has one configured origin: https://app.example.test. The helper requires exact equality with that value. It does not infer the trusted origin from an arbitrary incoming Host header. An apparently similar domain or an attacker-controlled suffix therefore does not qualify. Scheme and port representation are part of the deliberately narrow example policy as well.
A missing Origin value or the literal value null is rejected. This is a strict deployment decision for the particular browser workflow, not a claim that every legitimate client always supplies the same header. Before adopting it, verify supported browser journeys and intermediary infrastructure. Other client types need their own explicit contract rather than an undocumented relaxation added after the first compatibility report.
When Sec-Fetch-Site is present, the helper additionally requires same-origin; same-site is not treated as equivalent. If that supplementary metadata is absent, both the origin and session-token requirements still apply. These decisions are straightforward to test. They do not authenticate a person, and a direct non-browser client can set its own headers. They must not be marketed as a general method of distinguishing humans from automation.
The tested PHP helper
The code below is intentionally an isolated request-checking helper. It changes no account preference, sends no enquiry and makes no account-permission decision. That makes its accepted and rejected cases independently reproducible. The real application still needs to connect session handling, authorisation, business processing and truthful feedback. Check an established framework’s existing protection before introducing a separate custom implementation alongside it.
The helper permits only POST. Our workflow does not perform state-changing work through GET. POST by itself is not the CSRF defence: the additional requirements are what express this example’s protection contract. The function name formRequestAllowed means only that this particular set of checks passed. Volume admission or subsequent business validation may still reject the request before any customer setting is changed.
<?php
declare(strict_types=1);
// Baustein für eine kontrollierte Browseranwendung.
// Der feste Origin ist Konfiguration, kein eingehender Host-Header.
function formRequestAllowed(
string $method,
mixed $origin,
mixed $fetchSite,
mixed $submittedToken,
mixed $sessionToken,
string $trustedOrigin
): bool {
if ($method !== 'POST') return false;
if (!is_string($origin) || $origin !== $trustedOrigin) return false;
if ($fetchSite !== null && $fetchSite !== 'same-origin') return false;
if (!is_string($submittedToken) || !is_string($sessionToken)) return false;
if (preg_match('/\A[a-f0-9]{64}\z/D', $sessionToken) !== 1) return false;
if (preg_match('/\A[a-f0-9]{64}\z/D', $submittedToken) !== 1) return false;
return hash_equals($sessionToken, $submittedToken);
}
function ensureFormToken(array &$session): string {
$current = $session['form_token'] ?? null;
if (!is_string($current)
|| preg_match('/\A[a-f0-9]{64}\z/D', $current) !== 1) {
$session['form_token'] = bin2hex(random_bytes(32));
}
return $session['form_token'];
}
Integrate session state and output correctly
Configure session handling before output. For an HTTPS-only application, review Secure, HttpOnly and an intentionally selected SameSite policy for the session cookie. PHP’s strict session mode and cookie-only session use also belong in the integration review. These choices need to fit the real login architecture and any existing central session service. A project should not accidentally start an unrelated second session in a small added form handler.
When inserting the token into a hidden HTML attribute, escape for that output context. The example uses htmlspecialchars with ENT_QUOTES and UTF-8. Keep the token out of URLs that may appear in history, referrals or logs. Responses carrying session-specific form values also require appropriate cache handling so they are not inadvertently shared with other visitors. Review the deployed caching route rather than relying on the appearance of the form alone.
Lost session state is an ordinary failure case that needs a usable response. Do not report that a preference was successfully changed. Provide a clear route to a fresh form and retain non-sensitive entered information where appropriate. Whether reauthentication is needed follows from the actual session state. A silent retry without the required token must not become a convenience shortcut around the very condition being enforced.
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(ensureFormToken($_SESSION),
ENT_QUOTES, 'UTF-8') ?>">
Place SameSite and CORS in their proper roles
A SameSite cookie policy can add constraints to browser cookie sending. The suitable policy also depends on required authentication and integration journeys. Treat it as an additional layer rather than a reason to remove request checks. Another origin within the same site deserves particular attention. This helper’s exact origin requirement deliberately distinguishes origins more narrowly than an assumption that every related subdomain is interchangeable.
CORS controls browser access to responses and should not be described as preventing every request initiated elsewhere. A form can be submitted in ways that still require the application to protect its state-changing processing. For our workflow, the practical rule is simple: the receiving operation enforces its contract regardless of whether the initiating page would subsequently be allowed to read the response.
This distinction also helps API planning. A mobile application or external service needing the same business update gets a defined authentication and permission model. A browser session token is not casually repurposed as a general API credential. Different client contracts can share business validation without mixing their assumptions about cookies, browser context and identity. Documenting those boundaries makes later integrations easier to assess.
Treat automation as a separate admission problem
A script can retrieve a public form, keep cookies and return a correct token. That does not indicate a defect in token comparison; it describes a different route from a forged request initiated through someone else’s browser. The repair service now needs to manage accepted messages, delivery costs and handling capacity. Those quantities require their own controls and observations rather than a claim that tokens eliminate spam.
For an original planning exercise, we consider three admission dimensions: attempts per source, completed operations per account and a global limit before an expensive follow-up. Actual values must come from service capacity and legitimate peaks. A shared business connection may represent many real people, so an IP address is neither a dependable individual identity nor automatically a fair sole basis for rejecting enquiries.
The admission state must be updated atomically. If concurrent processes all read available capacity before any updates the count, the intended limit can be exceeded. A variable in one PHP process does not coordinate multiple workers or servers. The deployed service needs a suitable shared store and explicit concurrency tests. The small token helper does not claim to implement distributed request admission or prove its behaviour under load.
Bound expensive work and define retries
For the fictional public form, notifying the internal repair team may already involve a paid or rate-limited delivery service. Admission belongs before that work. Attachment processing, external lookups and excessive logging can introduce other costs. Trace the actual request path, including errors and retries. A limit applied only after an expensive operation may protect the visible queue while leaving the resource consumption largely untouched.
Repeated submissions also happen without an attacker. Someone clicks again after a slow response or retries after an interrupted connection. A costly operation therefore needs a business-level decision about repetition. A CSRF token is not automatically a unique operation identifier. Define when two submissions represent the same intended request and how an existing result is located without duplicating the underlying work.
HTTP 429 can represent a temporary volume rejection. The interface should explain a useful next step without exposing personal information or internal rules. A Retry-After value must reflect the actual retry opportunity. The repair business might provide a later retry and a separately maintained contact route. Claiming that an enquiry was sent even though admission rejected it would undermine both usability and operational understanding.
Use supplementary signals with proportionate care
A hidden honeypot control can identify some simple automated filling attempts. It is an additional heuristic, not proof that every remaining request came from a person. Our design review also asks whether legitimate autofill or assistive interaction could trigger it. A protection should not make access harder specifically for visitors who need help entering their information. Test false positives rather than assuming the technique is invisible to everyone.
A minimum completion time is likewise not a reliable human test. A legitimate visitor may paste a prepared message, while a script can wait. Such observations may contribute to a graduated decision but should not become an unexamined sole reason for permanent rejection. Name both sides of the trade-off: which genuine enquiries might be lost and which handling costs are actually avoided?
If an additional interactive check becomes necessary, its alternative route belongs in the design. Evaluate reduced unwanted work together with the burden on legitimate visitors. Our small service starts with clear fields, server rules and meaningful admission limits. Additional measures respond to observed needs rather than merely creating a conspicuous appearance of security beside the submission button.
Continue to validate content and handle output safely
A correct token does not make a message trustworthy. The server checks data types, agreed sizes and business values. A direct client can bypass an HTML length limit. The notification setting accepts only explicitly agreed choices rather than loosely converting unknown input into a convenient truth value. These checks belong to the business-processing stage after request protection and before any change is committed.
If an enquiry is later displayed to staff, its untrusted content still needs output handling appropriate to that context. Input validation is not a universal substitute for safe output. Likewise, a CSRF token does not repair script injection in the application itself. Keep the responsibilities distinct so that the team does not mistake an unrelated successful check for closure of another vulnerability.
Our handover therefore separates evidence into request checks, admission behaviour and business processing, with visible feedback reviewed alongside them. This gives each decision a clear owner. A passing token-test suite establishes nothing by itself about overlong messages, unauthorised customer identifiers or duplicate chargeable work. Those cases need their own expected outcomes and tests in the actual implementation.

Interpret the checks that were actually executed
The published pure PHP helper was exercised with 43 isolated checks. They cover matching session values, wrong methods, missing origins, deceptive domain shapes, different ports, supplementary metadata and malformed token parameters. Newly generated tokens meet the selected format. Valid stored tokens are retained and invalid stored state is replaced. The explanatory diagram uses concrete accepted and rejected outcomes from these checks.
An explicit default port in an incoming origin is intentionally rejected by this exact-string policy, as is a trailing slash. That behaviour is documented rather than silently normalised. A deployment needing several valid representations should define and test a narrow canonical origin policy. Substring matching is not an acceptable substitute for deciding which actual origins are trusted by the application.
These checks are not a load test, a real browser attack or acceptance of a production login flow. Integration needs additional HTTP checks with the actual session, proxy route and cache conditions. Admission limits need separate concurrency testing. Specific bounded evidence is more useful than a vague statement that one green test run makes every possible use of a form secure.
Prepare operational decisions and ownership
The repair business needs an owner for unusual admission patterns and failed follow-up work. Logs should provide the necessary events without casually collecting tokens, complete messages and unnecessary personal details. A technical request reference can support investigation of a rejection. The real organisation determines what information it needs and how long it should retain it with the relevant responsible specialists.
When incoming volume changes suddenly, distinguish genuine demand from unwanted load. A promoted offer or a shared company connection can produce a different pattern from ordinary traffic without being malicious. Operational intervention needs a recovery plan: temporary limits, controlled relief and review of legitimate rejections. Turning off every check in a hurry may replace a service interruption with a new exposure.
Acceptance ends with concrete answers. Who may perform each action? What rejects a forged browser request? What bounds automated volume before expensive processing? Which content rules apply? What does a person see after rejection or repetition? Answering and testing these questions together produces a sustainable form workflow rather than a loose collection of security-related attributes.
Protection responsibilities compared
| Question | Responsible decision |
|---|---|
| Who is authenticated? | Authentication and session |
| May this account be changed? | Specific authorisation |
| Does the request meet the browser contract? | Origin, session token and supplementary metadata |
| Can the volume be accepted? | Atomic shared admission before costly work |
| Are the values permitted? | Server-side business validation |
| Was this operation already completed? | Business-level repetition policy |
Pre-release checklist
- Document state-changing actions and specific permissions.
- Review existing framework protection first.
- Trace token generation, rendering, lifecycle and comparison.
- Configure trusted origins and verify the proxy route.
- Review actual session cookies and cache behaviour.
- Apply atomic admission before expensive work.
- Design retries and errors without false completion messages.
- Keep evidence for positive, negative and concurrent-load cases distinct.
Sources and review scope
Primary sources reviewed on 9 November 2026. The service application, repair business and workflow model are original fictional examples. The 43 isolated PHP checks concern only the displayed request helper and token generation.







