in_array Strict Mode
The PHP function in_array($needle, $haystack) does type juggling on the
needle. This is almost never what the developer meant when they wrote it.
The default behaviour
in_array(0, ["abc", "def", "ghi"]); // true on PHP < 8
in_array("a", [0, 1, 2]); // true on PHP < 8
in_array(true, ["whatever"]); // true (any non-empty string)Each of those is a bypass somewhere.
The strict form
in_array($needle, $haystack, true);
// ^^^^true flips it to strict mode — exact type and value match required, no
juggling. This is what the developer almost always meant.
A real-shape bypass
This pattern shows up in custom RBAC:
$banned_user_ids = [101, 202, 303];
if (in_array($_GET['user_id'], $banned_user_ids)) {
die("forbidden");
}
// continue with $_GET['user_id'] as if it were safeThe attacker sends ?user_id=abc. in_array("abc", [101, 202, 303]):
- PHP juggles
"abc"to0(PHP < 8 behaviour), 0is not in[101, 202, 303]→ returnsfalse,- the “banned” check passes,
- the rest of the code uses
"abc"as the user id.
Whether downstream code crashes or silently misbehaves depends on what it does with the id. Both outcomes are bugs.
A subtler version
$allowed_roles = ["admin", "editor"];
if (in_array(0, $allowed_roles)) {
// …
}in_array(0, ["admin", "editor"]) returns true on PHP < 8 because "admin"
juggles to 0. If anything ever passes 0 (an unsigned int default, a
missing query param coerced to int, a deserialised JSON null), the check
greenlights.
Detection during source review
Search patterns
# Non-strict in_array / array_search
rg -n 'in_array\(|array_search\(' --type=php | rg -v ', *(true|TRUE)\s*\)'Anything in the output that compares against user input or a security-relevant allow/deny list is a flag.
Cousin functions with the same problem:
array_search($needle, $haystack)— same juggling, plusfalsevs0return-value confusionarray_keys($array, $value)— also juggles by default; passstrict: truearray_unique($array, SORT_REGULAR)—SORT_REGULARjuggles; preferSORT_STRINGorSORT_NUMERIC
Test inputs
0falsenull"""0"(string zero)
Audit focus
For every non-strict in_array() / array_search() hit, decide whether
attacker input can bypass an allowlist or role check by coercing to one of
those values. Allowlists keyed on strings (["admin", "editor"]) collapse to
0 for any of the above; allowlists of integers ([101, 202]) collapse for
any non-numeric string the attacker sends.
In code review, treat any in_array(...) with fewer than three arguments
as suspicious until you can prove the needle and haystack are both fully
typed. It’s faster than tracing each one.
The fix
if (in_array($needle, $haystack, true)) { … }Or skip in_array entirely for security-critical checks and use a typed map:
$banned_ids = [101 => true, 202 => true, 303 => true];
if (isset($banned_ids[(int) $_GET['user_id']])) {
die("forbidden");
}The (int) cast neutralises the original juggling at the boundary, and the
isset lookup is O(1) and type-safe.
Quick reference
| Call | Risk |
|---|---|
in_array($u, $list) | ⚠️ Juggles. Bypass-prone. |
in_array($u, $list, true) | ✅ Strict. |
array_search($u, $list) | ⚠️ Juggles. Plus false/0 confusion. |
array_search($u, $list, true) | ✅ Strict. |
isset($map[$u]) | ✅ When $map is keyed by allowed values. |