How to Sanitize Form Input in PHP: Validate Input, Escape Output

Quick Summary

  • Validate each form field when it arrives (present, a string, the right length or format) and escape it when you print it with htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ).
  • FILTER_SANITIZE_STRING is deprecated since PHP 8.1 and cuts text at a < sign; use htmlspecialchars() when printing, or FILTER_SANITIZE_FULL_SPECIAL_CHARS only for values that go straight to the page.
  • Check emails, numbers and yes/no values with filter_var() and FILTER_VALIDATE_*, and send values to the database through prepared statements (PDO, mysqli or $wpdb->prepare() in WordPress).

To handle form input safely in PHP, check each field when it arrives (is it there, is it a string, does it have the length or format you expect?) and escape it when you print it: htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) in HTML, and a prepared statement when it goes into a database. Don’t use FILTER_SANITIZE_STRING for this any more: it’s deprecated since PHP 8.1 and cuts ordinary text. The examples below were run on PHP 8.2 and 8.4.

What replaces FILTER_SANITIZE_STRING?

Older code, including the first version of this post, cleaned the fname and lname fields with filter_input( INPUT_POST, 'fname', FILTER_SANITIZE_STRING ). On PHP 8.1 and later that line prints “Deprecated: Constant FILTER_SANITIZE_STRING is deprecated”. The filter also removes more than tags: it treats everything after a < as a tag, so “I think 5<6” comes out as “I think 5”. What to use instead depends on what you want:

PHP
$input = "I think 5<6 & \"O'Brien\" <b>agrees</b>";

echo htmlspecialchars( $input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ), "\n";
// I think 5<6 & "O'Brien" <b>agrees</b>

echo filter_var( $input, FILTER_SANITIZE_FULL_SPECIAL_CHARS ), "\n";
// I think 5<6 & "O'Brien" <b>agrees</b>

echo filter_var( $input, FILTER_SANITIZE_SPECIAL_CHARS ), "\n";
// I think 5<6 & "O'Brien" <b>agrees</b>

echo strip_tags( $input ), "\n";
// I think 5
  • htmlspecialchars() converts &, ", ', < and > into HTML entities, so the browser shows the text as it was typed and runs none of it. It’s the replacement php.net recommends. Use it when you print a value, not when you save it.
  • FILTER_SANITIZE_FULL_SPECIAL_CHARS does the same encoding as htmlspecialchars() with ENT_QUOTES, but while reading the input.
  • FILTER_SANITIZE_SPECIAL_CHARS uses numeric entities and also encodes characters below ASCII 32, such as a tab or a line break.
  • strip_tags() removes tags instead of encoding them, with the same “5<6” problem, and it leaves quotes alone. Its result still needs escaping before it goes into an HTML attribute.

How do you validate and escape a form field?

This is the post’s first name and last name form, written the current way. Save it as one PHP file, for example form.php: it shows the form and handles the submission.

PHP
<?php
function escape_html( $value ) {
    return htmlspecialchars( (string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
}

function post_string( $key ) {
    $value = $_POST[ $key ] ?? '';
    return is_string( $value ) ? trim( $value ) : '';
}

$fname  = '';
$lname  = '';
$errors = [];

if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
    $fname = post_string( 'fname' );
    $lname = post_string( 'lname' );

    // 1 to 50 characters of valid UTF-8, on one line.
    if ( ! preg_match( '/^.{1,50}$/u', $fname ) ) {
        $errors[] = 'Enter a first name of up to 50 characters.';
    }
    if ( ! preg_match( '/^.{1,50}$/u', $lname ) ) {
        $errors[] = 'Enter a last name of up to 50 characters.';
    }
}
?>
<form method="post">
    <input type="text" name="fname" value="<?= escape_html( $fname ) ?>">
    <input type="text" name="lname" value="<?= escape_html( $lname ) ?>">
    <button type="submit">Send</button>
</form>
<?php foreach ( $errors as $error ) : ?>
    <p><?= escape_html( $error ) ?></p>
<?php endforeach; ?>
<?php if ( 'POST' === $_SERVER['REQUEST_METHOD'] && ! $errors ) : ?>
    <p>Hello, <?= escape_html( "$fname $lname" ) ?>!</p>
<?php endif; ?>
  • post_string() reads a field with ?? '', so a missing field becomes an empty string instead of a warning. It also turns an array into an empty string: anyone can send fname[]=x, and trim() on an array stops PHP 8 with “Uncaught TypeError: trim(): Argument #1 ($string) must be of type string, array given”.
  • The preg_match() check is the validation. The u modifier counts characters rather than bytes and fails on invalid UTF-8, and . doesn’t match a line break. A value that fails is rejected with a message rather than silently changed. Use the rule that fits each field: a list of allowed values, a number range or an email format (see below).
  • escape_html() runs on every value the page prints, in the value attributes and in the text.

With "><script>alert(1)</script> as the first name, the page prints this, and the browser shows the text instead of running it:

HTML
<input type="text" name="fname" value=""><script>alert(1)</script>">

On PHP 8.1 and later, ENT_QUOTES | ENT_SUBSTITUTE and UTF-8 are already the defaults, so htmlspecialchars( $value ) gives the same result. Writing them out does no harm, but don’t write ENT_QUOTES alone: without ENT_SUBSTITUTE, a string with one invalid UTF-8 byte comes back empty.

The first version of this post checked isset( $_POST['fname'] ) && ! empty( $_POST['fname'] ) and then read the value from $_REQUEST. empty() already includes the isset() check, and it treats the string "0" as empty, so a field containing just 0 was skipped. Reading $_POST in both places is also clearer.

When should you use filter_input()?

filter_input() reads one field straight from the request and runs a filter on it. With the replacement filter, the old lines become:

PHP
$fname = filter_input( INPUT_POST, 'fname', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
$lname = filter_input( INPUT_POST, 'lname', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
  • It returns null when the field wasn’t sent, hence the ?? '', and false when a filter fails.
  • The result is already HTML-encoded: O'Brien <b>Tom</b> arrives as O&#039;Brien &lt;b&gt;Tom&lt;/b&gt;. That’s safe to print, but don’t escape it again (the page would show O&#039;Brien) and don’t save it, or your database, emails and exports fill up with entities.
  • It reads the request as PHP received it: changes you make to $_POST don’t affect it, and on the command line, where there is no request, it returns null. To filter a value you already have in a variable, use filter_var().

Because of the second point, the form above keeps the raw value and escapes it when printing. Use filter_input() with FILTER_SANITIZE_FULL_SPECIAL_CHARS only when the value goes straight to the page.

How do you validate emails, numbers and URLs?

filter_var() with a FILTER_VALIDATE_* filter returns the value, converted to the right type where that makes sense, or false when it isn’t valid. In a form, pass $_POST['email'] ?? '' instead of the sample strings:

PHP
$email = filter_var( 'tom@example.com', FILTER_VALIDATE_EMAIL ); // "tom@example.com"
$bad   = filter_var( 'tom@', FILTER_VALIDATE_EMAIL );            // false

$range = [ 'options' => [ 'min_range' => 18, 'max_range' => 120 ] ];
$age   = filter_var( ' 42 ', FILTER_VALIDATE_INT, $range ); // int(42)
$young = filter_var( '12', FILTER_VALIDATE_INT, $range );   // false

$agree = filter_var( 'yes', FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE );   // true
$maybe = filter_var( 'maybe', FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ); // null

$url    = filter_var( 'javascript://comment%0Aalert(1)', FILTER_VALIDATE_URL ); // passes
$scheme = parse_url( (string) $url, PHP_URL_SCHEME );                         // "javascript"
$link   = in_array( $scheme, [ 'http', 'https' ], true ) ? $url : '';        // ""

$color = 'green';
$valid = in_array( $color, [ 'red', 'green', 'blue' ], true ); // true

var_dump( $email, $bad, $age, $young, $agree, $maybe, $link, $valid );
  • FILTER_VALIDATE_INT trims the string, checks the range and returns an int. Test the result with false === $age, since 0 can be a valid number.
  • FILTER_VALIDATE_BOOL is the PHP 8.0 name of FILTER_VALIDATE_BOOLEAN. With FILTER_NULL_ON_FAILURE, null means “not a yes/no value” and false means “no”.
  • FILTER_VALIDATE_URL accepts any scheme, including javascript:, so check the scheme before you put a submitted URL in an href.
  • FILTER_VALIDATE_EMAIL checks the format only. The only way to know an address works is to send it an email.
  • For select menus, radio buttons and checkboxes, compare the value with the list of options you offered, with in_array() and true as the third argument.

How do you save form input to a database?

Use a prepared statement and pass the values separately. The database treats them as data, so quotes or SQL inside a field can’t change the query, and you don’t need to escape or strip anything for SQL:

PHP
// On a real site: $pdo = new PDO( 'mysql:host=localhost;dbname=mysite;charset=utf8mb4', $db_user, $db_pass );
$pdo = new PDO( 'sqlite::memory:' ); // an in-memory SQLite database for this demo
$pdo->exec( 'CREATE TABLE contacts (fname TEXT, lname TEXT)' );

$fname = "Robert'); DROP TABLE contacts;--";
$lname = "O'Brien";

$stmt = $pdo->prepare( 'INSERT INTO contacts (fname, lname) VALUES (?, ?)' );
$stmt->execute( [ $fname, $lname ] );

$stmt = $pdo->prepare( 'SELECT fname, lname FROM contacts WHERE lname = :lname' );
$stmt->execute( [ 'lname' => $lname ] );
print_r( $stmt->fetchAll( PDO::FETCH_ASSOC ) );

The table keeps both values exactly as they were typed, including the fake SQL. Since PHP 8.0, PDO throws a PDOException when a query fails, so errors don’t pass unnoticed. With mysqli, use prepare() and bind_param(), or $mysqli->execute_query( $sql, [ $fname, $lname ] ) on PHP 8.2 and later.

Save the validated value as it is and escape it when you print it. The same data then works in HTML, emails, JSON and CSV exports.

How do you sanitize form input in WordPress?

Inside WordPress, sanitize_text_field() for incoming values and esc_html() or esc_attr() when printing are the usual choices. WordPress adds backslashes to $_POST, so remove them first with wp_unslash():

PHP
$fname = isset( $_POST['fname'] ) ? sanitize_text_field( wp_unslash( $_POST['fname'] ) ) : '';

echo '<input type="text" name="fname" value="' . esc_attr( $fname ) . '">';

For the input O'Brien <b>Tom</b>, this gives O'Brien Tom; without wp_unslash() you’d get O\'Brien Tom. sanitize_text_field() also returns an empty string when the field is an array. For SQL, use $wpdb->prepare() with %s and %d placeholders, or $wpdb->insert(). Which of esc_html, esc_attr and esc_url you need depends on where the value is printed: as text, in an attribute or as a URL.

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More