A common pattern in web development is a front-end page that fires an AJAX request to a PHP file to fetch or submit data — something like data.php. The problem: that file is just a URL. Anyone who finds it — including via browser dev tools — can call it directly, bypassing whatever checks your front-end page normally enforces. Here's how to close the actual gaps, in order of importance.
1. Always Use POST, Never GET
GET parameters end up in the URL, server logs, browser history, and referrer headers — all places sensitive data shouldn't be. POST keeps parameters in the request body and makes casual replay (copy-pasting a URL) much harder:
$.ajax({
url: 'data.php',
method: 'POST',
data: { name: 'value' },
success: function(response) {
$('#output').html(response);
}
});
From here, your PHP file should reject anything that isn't a POST request.
2. Gate Access with PHP Sessions
Use server-side sessions for authentication state, not cookies — cookies can be read and modified client-side; sessions keep the sensitive part on the server and only send a session ID to the browser.
<?php
session_start(); // must be the very first line, before any output
if (!isset($_SESSION['user_id'])) {
header('Location: https://yoursite.com/login');
die();
}
3. Add CSRF Protection
Cross-Site Request Forgery works like this: a user logs into your site, then — while still logged in — visits a different, malicious page. That page silently fires a request to your endpoint using the browser's active session cookie. Your server can't tell it apart from a real request, because the login state alone doesn't prove the request came from your own page.
The fix is a token the attacker can't predict:
// On the page that makes the AJAX call:
<?php
session_start();
$csrf = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $csrf;
?>
// In the AJAX call:
$.ajax({
url: 'data.php',
method: 'POST',
data: {
name: 'value',
csrf: '<?php echo $csrf; ?>'
}
});
// In data.php:
session_start();
if (!isset($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf']) {
header('Location: https://yoursite.com');
die();
}
A forged request from another origin can't know this token, so it fails validation immediately.
4. Always Pair header() Redirects with die()
This is the detail most developers miss. header('Location: ...') queues a redirect — it does not stop the script from executing. A normal browser follows the redirect and never sees what comes after, but a tool like curl or Burp Suite can simply ignore the redirect header and read the full response, including anything your script outputs afterward.
// WRONG — code below still runs
header('Location: https://yoursite.com');
// CORRECT — nothing after this executes
header('Location: https://yoursite.com');
die();
5. Rate-Limit with a Session Counter
Even with CSRF protection, a valid session can be replayed rapidly using tools like Burp Suite. Cap how many times a single session can call the endpoint:
if (!isset($_SESSION['dos_count'])) {
$_SESSION['dos_count'] = 0;
}
if ($_SESSION['dos_count'] >= 3) {
die('Too many requests. Please wait.');
}
$_SESSION['dos_count']++;
Add a time-based reset so legitimate users aren't locked out permanently:
$now = time();
if (!isset($_SESSION['dos_time'])) {
$_SESSION['dos_time'] = $now;
}
if ($now - $_SESSION['dos_time'] > 60) {
$_SESSION['dos_count'] = 0;
$_SESSION['dos_time'] = $now;
}
This only protects against a single session flooding the endpoint — actual distributed attacks (DDoS) need server-level mitigation (a WAF, Cloudflare, nginx rate limiting), which is outside what PHP alone can handle.
6. Block Direct Access at the Server Level Too
The protections above run once PHP executes — but you can also block direct access to specific files before PHP even runs, using an .htaccess rule (on Apache) as an extra layer:
# In .htaccess, in the same directory as data.php
<Files "data.php">
Order Deny,Allow
Deny from all
</Files>
This blocks all direct requests to the file entirely — which only works if your application accesses it internally (e.g., via an include or a server-side redirect) rather than the browser calling it directly. If your setup requires the browser to call data.php directly via AJAX (the standard case described above), skip this and rely on the session/CSRF/rate-limit checks instead — the two approaches solve different scenarios and aren't both applicable at once.
Putting It All Together
<?php
session_start(); // must be first line
// Reject non-POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: https://yoursite.com');
die();
}
// Validate CSRF token
if (!isset($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf']) {
header('Location: https://yoursite.com');
die();
}
// Rate limiting
if (!isset($_SESSION['dos_count'])) {
$_SESSION['dos_count'] = 0;
}
if ($_SESSION['dos_count'] >= 3) {
die('Too many requests.');
}
$_SESSION['dos_count']++;
// Your actual logic goes here
echo "Hello, " . htmlspecialchars($_POST['name']);
Quick Checklist
session_start()as the very first line of every protected file- Sessions for auth — never cookies
- CSRF token generated with
random_bytes(), validated on every request - POST only, never GET, for sensitive data
die()immediately after everyheader('Location: ...')- Session-based rate limiting on top of everything else
- Remember PHP file names are case-sensitive on most servers —
data.phpandData.phpare different files