PHP Form Validation, CSRF Token and Duplicate Submission Debugger
Find whether a broken or unsafe PHP form is caused by client-only validation, a missing session, an expired or regenerated CSRF token, a wrong field name, a request-format mismatch, refresh resubmission, two JavaScript handlers, an unsafe GET action, or missing database-level duplicate protection.
Describe the Form Failure
Complete the fields you know. The optional code areas improve pattern detection, but the tool can still produce a useful debugging order without code.
Your PHP Form Debugging Result
Waiting for analysis
Complete the form and run the debugger.
Recommended response pattern: Not calculated
Ranked Likely Causes
Ordered Fix Plan
Reusable CSRF helper
Safe HTML form and PHP handler
Fetch request and JSON endpoint
POST-only destructive action
Server-side validation pattern
Duplicate-submission controls
How to Debug PHP Form Validation, CSRF Tokens and Duplicate Inserts
A PHP form is a chain of separate decisions. The browser builds a request, PHP reads the request, the session supplies trusted state, validation checks the submitted values, authorization decides whether the action is allowed, the database performs the change, and the response tells the browser what to do next. The same visible symptom can come from different layers, so the fastest debugging method is to collect evidence in that order.
HTML Validation and PHP Validation Solve Different Problems
Attributes such as required, min, max, type="email" and pattern improve the browser experience. They can stop an ordinary user from submitting an obviously incomplete form and can display quick feedback before a request is sent. They do not establish a trusted security boundary. A user can edit the page in developer tools, disable JavaScript, send a request with Postman, replay a saved request or call the PHP file directly.
The PHP handler must therefore validate every value it relies on. Syntactic validation checks whether the input has the expected format, such as a valid integer, email address or date. Semantic validation checks whether the value makes sense for the feature, such as a quantity being available, a booking end time occurring after the start time, or a selected category existing in the database. Sanitizing output and using prepared statements remain separate requirements. Validation does not automatically prevent SQL injection or cross-site scripting.
When a form appears to submit but PHP receives empty values, compare the exact HTML name attributes with the keys read by PHP. An element ID does not become a request key unless the element also has a matching name. Disabled controls are not normally submitted. A JSON request does not populate $_POST in the same way as a standard encoded form, and a file upload requires multipart/form-data.
What a CSRF Token Proves
Cookie-based login sessions are normally attached to requests by the browser. That convenience creates a risk: another site may try to cause the logged-in browser to submit an unwanted state-changing request. A CSRF token adds a value that the attacking site should not know. The application stores an unpredictable token in trusted server-side state, places it in the legitimate form or request header, and compares the submitted value before changing data.
A token does not replace authentication, authorization, validation or prepared statements. It answers a narrower question: did this state-changing request include the expected anti-forgery value for this user session or form flow? The handler still needs to confirm who the user is, whether that user may perform the action, whether the request method is correct and whether every submitted value is valid.
Generate tokens with a cryptographically secure source such as random_bytes(), then encode the bytes for HTML or transport. Do not use a constant, a username, a timestamp, rand() or a predictable counter. Compare the known server value with the submitted value using a deliberate comparison such as hash_equals(). The user-supplied value belongs in the second argument.
Why a Valid-Looking Token Produces “Mismatch”
A mismatch often means the form and handler are not reading the same session or the same token generation event. The form may be rendered before session_start(). One file may use $_SESSION['csrf_token'] while another checks $_SESSION['token']. The login flow may regenerate or replace the session without preserving expected values. A shared include may regenerate the token each time it is loaded, so the value stored during the POST request is different from the value printed into the form.
Debug the actual values without exposing them publicly. In a local environment, log whether the expected token exists, whether the submitted token exists, their lengths, the session ID prefix and the request method. Do not print full tokens into a production response. A missing token and a mismatched token are different failures. The first points to rendering, field naming or request parsing. The second points to lifecycle, session or comparison logic.
Another common error is generating a new token at the top of every request before checking whether the request is POST. The browser submits token A, the handler starts and immediately stores token B, then compares submitted A with expected B. Generate the token only when it does not exist, or use an intentional per-form lifecycle where the expected token is retained until comparison.
Multiple Tabs, Back Buttons and One-Time Tokens
A single session token is simple and works across multiple open forms, but it remains valid for longer. A one-time token can provide narrower replay protection, but careless rotation causes usability bugs. If opening tab B replaces the token that was printed in tab A, submitting tab A will fail even though the user did nothing suspicious. The same issue appears when a user returns with the Back button to a cached form.
Choose the lifecycle deliberately. A basic student project can reuse one unpredictable session token and rotate it after login or another meaningful boundary. A higher-risk action can store separate tokens per form or action. A one-time token should be consumed only after successful verification and should produce a clear recovery path when stale. Do not silently regenerate every token on every include.
Why One Click Creates Two Database Rows
Duplicate inserts require evidence from both the browser and server. Open the Network tab and count POST requests. Two requests usually point to a form submit plus a JavaScript fetch, two event listeners, a button with both inline and registered handlers, or a double click before the UI changes. One POST that produces two rows points to PHP or database logic, such as the insert function being called twice, the handler being included twice, a retry path repeating the query, or a trigger creating another row.
Disabling the submit button improves the interface, but it is not reliable server protection. A user can send another request, the connection can retry, or two tabs can submit the same logical action. Use multiple layers according to the feature: prevent accidental double clicks in the browser, redirect after a successful normal POST, add a meaningful database UNIQUE constraint, and use a server-side submission key for operations where repeated requests must return the original result rather than perform the action again.
Do not add a random UNIQUE column that changes on every request and expect it to prevent duplicates. The constraint must represent the business rule, such as one registration per email, one enrollment per student and course, or one order processing record per checkout submission key.
Post/Redirect/Get Stops Refresh Resubmission
When a normal HTML form renders a success page directly from the POST response, refreshing the page may repeat the POST. The browser warns about resubmitting form data because the current history entry is still the mutation request. The Post/Redirect/Get pattern performs the change, stores any flash message, returns a redirect, and lets the browser load the final page with GET.
An explicit 303 See Other is clear after a successful POST because the redirected request is retrieved with GET. In plain PHP, send the Location header before output and call exit immediately. PRG reduces accidental refresh duplicates, but it does not replace database constraints or idempotency handling. Two separate POST requests can still reach the server before either redirect is processed.
Delete, Approve and Update Should Not Be GET Links
A URL such as delete.php?id=5 is easy to build, but GET is intended for retrieval and can be followed by crawlers, preview tools, browser prefetching, history and external pages. A destructive action should use the intended state-changing method, confirm the logged-in user and permission, validate a CSRF token, validate the record ID and apply ownership or role restrictions before the query runs.
Changing the link to POST is necessary but not sufficient. Another site can submit a hidden POST form. The handler still needs anti-forgery validation. Likewise, a JavaScript confirmation dialog does not protect the endpoint because the endpoint can be called without the original button.
Sending CSRF Tokens with Fetch and AJAX
A fetch request can send the token inside FormData, a JSON body or a custom header. The PHP endpoint must read the same location. If JavaScript sends JSON, PHP normally reads and decodes php://input; looking only in $_POST produces a missing-token error. If JavaScript uses FormData, do not manually replace the browser-generated multipart Content-Type boundary.
Return one controlled JSON response. Do not mix warnings, HTML login pages, redirect output and JSON. A session or token failure can return a suitable status such as 401 for missing authentication, 403 for forbidden or invalid anti-forgery validation according to the project’s response policy, and 422 for validation errors. The frontend should inspect response.ok and parse the body only according to the declared response format.
For cross-origin requests, cookie behavior and CORS configuration add another layer. A CSRF token is not a reason to allow every origin, and CORS is not a substitute for CSRF protection. Keep the allowed origins, credentials policy, cookie attributes and token design consistent with the real deployment.
SameSite Cookies Help, but They Are Not the Whole Design
SameSite cookie settings can reduce when browsers attach cookies to cross-site requests. They are useful defense in depth, but application behavior, browser compatibility, subdomain relationships and same-site request contexts still matter. Do not remove server-side token validation from sensitive cookie-authenticated actions merely because a cookie is Lax or Strict.
If the application requires SameSite=None, the cookie must also use Secure in modern browsers. Test the actual login and form flow over HTTPS. On localhost, document the differences between the development environment and production cookie configuration rather than changing security settings randomly until the form works.
A Safe PHP Form Processing Order
- Start or resume the intended session before reading session data and before output.
- Confirm the user is authenticated when the feature requires login.
- Confirm the request method and reject unexpected methods.
- Read the body according to its real encoding: standard form, multipart form or JSON.
- Confirm the expected CSRF token and submitted token both exist.
- Compare the token before performing the state change.
- Validate required fields, types, ranges and business rules on the server.
- Authorize the user for the action and specific record.
- Use prepared statements and suitable database constraints.
- Check execution results, commit the intended change, rotate or consume tokens according to policy, then return JSON or redirect with 303 and exit.
This order prevents a common class of partial fixes. For example, adding a CSRF field does not help when the handler still deletes through GET. Adding a redirect does not help when the browser sends two POST requests. Adding required does not help when a direct request bypasses the form. Each control belongs to a specific failure layer.
Common Fixes That Create New Problems
- Removing token validation because it blocks the form instead of finding the session or lifecycle mismatch.
- Generating a new token immediately before comparison.
- Placing tokens in URLs where they can appear in history, logs and referrer data.
- Using
uniqid(), timestamps or a static string as a security token. - Trusting JavaScript validation or a disabled button as the only protection.
- Using GET for delete, approval, status changes or password changes.
- Showing success because the script reached the bottom without checking the query result.
- Retrying every failed request automatically, including non-idempotent inserts.
- Using output buffering to hide a redirect warning without locating the earlier output.
- Storing a raw database or validation error in the public response.
Viva Questions and Answers
What is server-side validation? It is the validation performed by PHP after the request reaches the server. It cannot be bypassed by disabling browser validation and should check format, required values and business rules.
What is CSRF? It is a request-forgery risk where a browser that already has a login cookie is caused to send an unwanted state-changing request. A CSRF token helps the server distinguish the intended form flow from a forged request.
Why use Post/Redirect/Get? It changes the browser’s final history entry to a GET page after a successful POST, reducing accidental resubmission when the user refreshes.
Why can duplicate rows still occur after disabling the button? Client controls can be bypassed, two requests may already be in flight, or server code may execute the insert twice. Database constraints and server-side idempotency address the data layer.
Why is a prepared statement not enough? Prepared statements protect query structure from injected values. They do not validate business rules, prove permission, prevent CSRF or guarantee that a logical action runs only once.

