These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
PHP Securityintval() Bypass

intval() / (int) Bypass

intval("123abc") returns 123. (int)"123abc" returns 123. The non-numeric tail is dropped without a warning. When code uses intval to “sanitise” an ID for one query but uses the raw string for a second query, display, or ownership check, the two views diverge and access control breaks.

Why

The classic shape:

$raw = $_GET['id'];
$id  = intval($raw);
 
$row = $db->find($id);                     // intval'd — points at row 1
display("Editing item: $raw");             // raw — shows "1abc"
audit_log("user edited $raw");             // raw — corrupts logs
 
if ($row->owner_id == $_SESSION['user_id']) {
  $db->update($raw, $_POST);               // raw again — depending on driver,
}                                          // may hit a different row or fail

123abc and 123 are simultaneously “the same row” and “different strings” depending on which line you’re reading.

A second flavour: when the integer cast is used as a boolean (“non-zero means logged in”), strings starting with non-digits become 0:

$logged_in = intval($_COOKIE['session']);   // attacker sets cookie to "abc"
if ($logged_in) {  }                       // 0 — fine, but…

Inverse cases are worse — when 0 is “guest” but 0 also means “valid”, the application leaks state.

Search patterns

rg -n 'intval\(|\(int\)|\(integer\)' --type=php
rg -n 'settype\(\s*\$\w+,\s*[''"]integer[''"]\s*\)' --type=php
 
# floatval too — same family
rg -n 'floatval\(|\(float\)|\(double\)' --type=php

Test inputs

Inputintval resultNote
123abc123trailing garbage dropped
0abc0starts with 0
admin1230non-digit prefix → 0
1 OR 1=11SQLi survives if $raw is the one used in query
1' OR 1=1--1same
1e51not 100000intval doesn’t read scientific
" 123"123leading whitespace trimmed
-1-1may unlock unsigned-int rows / wrap downstream
99999999999999999999PHP_INT_MAXclamped on 64-bit
0x1A0hex not parsed by default
01010octal not parsed (changed in PHP 7)

Audit focus

For each intval / (int) hit:

  1. What is the cast result used for? Authorization decision, payment validation, object ownership, array index?
  2. Is the original string also used later? Display, second query, log line, redirect, email? Two views of the same parameter is the canonical bug shape.
  3. Is there a strict validation paired with the cast? ctype_digit, filter_var(..., FILTER_VALIDATE_INT), regex — anything that rejects on failure rather than silently zeroing.
  4. Negative numbers — does -1 (or PHP_INT_MIN) unlock unexpected rows or wrap to large unsigned values downstream?
  5. Range — does the integer feed an array index that could be very large or negative?

Fix

Validate, don’t sanitise. Reject inputs that aren’t already valid:

$id = filter_var($_GET['id'], FILTER_VALIDATE_INT, [
  'options' => ['min_range' => 1, 'max_range' => PHP_INT_MAX],
]);
if ($id === false) {
  http_response_code(400);
  exit;
}

Then use $id everywhere — never the raw string.

⚠️

When a framework’s router converts {id} segments via type-cast, you may still get intval-like behaviour at the controller boundary. Check the router config — Laravel’s where('id', '[0-9]+') constraint is the right default for numeric routes.