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:
$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 5htmlspecialchars()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_CHARSdoes the same encoding ashtmlspecialchars()withENT_QUOTES, but while reading the input.FILTER_SANITIZE_SPECIAL_CHARSuses 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
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 sendfname[]=x, andtrim()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. Theumodifier 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 thevalueattributes 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:
<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:
$fname = filter_input( INPUT_POST, 'fname', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';
$lname = filter_input( INPUT_POST, 'lname', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ?? '';- It returns
nullwhen the field wasn’t sent, hence the?? '', andfalsewhen a filter fails. - The result is already HTML-encoded:
O'Brien <b>Tom</b>arrives asO'Brien <b>Tom</b>. That’s safe to print, but don’t escape it again (the page would showO'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
$_POSTdon’t affect it, and on the command line, where there is no request, it returnsnull. To filter a value you already have in a variable, usefilter_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:
$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_INTtrims the string, checks the range and returns anint. Test the result withfalse === $age, since 0 can be a valid number.FILTER_VALIDATE_BOOLis the PHP 8.0 name ofFILTER_VALIDATE_BOOLEAN. WithFILTER_NULL_ON_FAILURE,nullmeans “not a yes/no value” andfalsemeans “no”.FILTER_VALIDATE_URLaccepts any scheme, includingjavascript:, so check the scheme before you put a submitted URL in anhref.FILTER_VALIDATE_EMAILchecks 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()andtrueas 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:
// 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():
$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.
Related articles
- reading form values with $_GET, $_POST and $_REQUEST: where the values come from
- checking an uploaded image’s type and size: validation for file fields
- preventing form resubmission on refresh: redirect after a successful POST

