skip to content

Dangerous Sinks

eval, include, unserialize and extract turn attacker input into code or state, and a loose == can wave a forged hash through. Interviewers probe spotting these calls and the safer alternatives.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, which built-in calls turn attacker-controlled request data into executed code or instantiated objects, and why are they dangerous?

level: juniorimportance: must knowfreq 58%

answer

  1. they run or build code from a string
  2. eval, include, unserialize, extract
  3. eval is a language construct, not a function
  4. unserialize can instantiate any class
  5. extract writes request keys into variables

basics

~10 s

eval() runs a string as PHP; include/require execute a file you supply; unserialize() rebuilds objects and calls their magic methods; extract() writes request keys into variables. Never feed request data to any of them.

solid answer

~40 s

The classic PHP **sinks** each take a string and turn it into behaviour. `eval($s)` compiles and runs `$s` as PHP. `include`/`require` execute the file at a path you give them, so a request-controlled path is remote or local file inclusion. `unserialize($s)` reconstructs whatever object graph the bytes describe and invokes magic methods (`__wakeup`, `__unserialize`, `__destruct`) during and after — an attacker who controls the bytes drives your object lifecycle. `extract($_GET)` and variable variables (`$$name = ...`) create local variables named by request keys, clobbering things like `$isAdmin`. A weak comparison (`md5($x) == $stored`) is a subtler sink that can wave a forged value through. The rule is the same for all: keep untrusted input out of the argument, and prefer a data format (JSON) or an allow-list over a call that interprets the input.

go deeper

for a junior

Recall the five sinks by name: eval, include/require, unserialize, extract, and loose comparison of secrets.

for a middle

Explain the primitive each hands an attacker — code execution, object instantiation, file inclusion, variable overwrite — and why filtering the string doesn't fix it.

for a senior

In a review, trace request data to the sink and propose the structural fix (allow-list, JSON, hash_equals) rather than a sanitiser, and check uploaded files can't reach an include.

for a principal

Weigh the cost of eliminating a legacy sink across a codebase against wrapping it, and decide where the trust boundary and enforcement belong.

## What a "sink" is here A **sink** is a call that treats its argument as more than data — as code to run, a file to execute, an object graph to rebuild, or a variable name to bind. When attacker-controlled input reaches one, the attacker is no longer choosing a *value*; they are choosing *what your program does*. PHP ships several, and interviewers screen for whether you can name them and say why each is dangerous. ## The core five - **`eval($string)`** — compiles and runs the string as PHP. It is a **language construct, not a function**, so it returns `null` unless the code calls `return`, throws a `ParseError` on a syntax error (since PHP 7), and — importantly — **cannot be listed in `disable_functions`**, because that directive only disables *functions*. There is no in-process way to sandbox arbitrary user PHP. - **`include` / `require` (and the `_once` forms)** — execute the file at the given path. A request-controlled path is file inclusion: a local file (source disclosure, or an uploaded file run as PHP) or, if `allow_url_include` is on, a remote URL. - **`unserialize($string)`** — rebuilds the value the bytes describe. If the bytes describe objects, PHP **instantiates those classes and calls their magic methods**. An attacker who controls the string picks which objects come to life. - **`extract($array)` and variable variables (`$$key`)** — create variables whose *names* come from the data. `extract($_GET)` lets a request define any local variable, overwriting one your logic depends on. A sixth, quieter case: a **loose comparison** (`==`, or `in_array` without the strict flag) used on a secret or hash lets certain forged inputs compare equal. ## Why "just validate the string first" is the wrong instinct You cannot reliably validate a string into safety when the sink is what interprets it. A regex that tries to allow "safe" PHP for `eval`, or to strip `../` before an `include`, is a filter racing an interpreter, and the interpreter has more moves. The durable fix removes the sink from the untrusted path: | Sink | Safe replacement | |---|---| | `eval` on input | a data structure + `match`/a dispatch table; there is no safe eval of user code | | `include` with a user path | an **allow-list** mapping request tokens to fixed file paths | | `unserialize` on input | `json_decode`/`json_encode` — JSON builds no objects | | `extract($_GET)` | read the specific keys you expect by name | | `md5($x) == $h` | `hash_equals()` for tokens, or `===` for exact match | ## How this shows up in an interview You are usually handed a snippet and asked "what is wrong with this?" The answer names the sink, states the primitive it hands the attacker (code execution, object instantiation, file read/inclusion, variable overwrite), and gives the structural fix — not a filter. Being able to *spot* the call is the junior bar; explaining the object-instantiation and allow-list details is the middle-and-up bar covered by the sibling questions.

  • Why can't `disable_functions` block `eval()`?
    `disable_functions` only disables *internal functions*. `eval` is a language construct, not a function, so it is not on the list of things that directive can touch. You remove the risk by not passing untrusted input to it, not by configuration.
  • Is `include`-ing a local uploaded file safe because it is not a remote URL?
    No. If an attacker can upload a file (even disguised as an image) and then get its local path into an `include`, the PHP inside it runs. Local file inclusion is code execution too; `allow_url_include` only governs the *remote* URL case.

saying these in an interview costs you the question

  • eval is fine if you filter the string first
  • disable_functions can turn off eval to make it safe
  • unserialize only rebuilds arrays, never runs anything
  • include is safe as long as the path is a local file
  • extract is just a convenient way to read $_GET
open as a page

A legacy PHP app unserializes a 'remember me' cookie on every request; why is that a remote-code-execution risk, and does unserialize's allowed_classes option fix it?

level: seniorimportance: must knowfreq 52%

basics

~20 s

unserialize() rebuilds whatever object graph the cookie bytes describe and calls magic methods like __wakeup and __destruct, so attacker-chosen objects run code (a gadget chain). allowed_classes narrows which classes are built but PHP still warns against untrusted input; replace the cookie with signed JSON.

open as a page

In PHP, why does building an include/require path from request input risk code execution, and what do allow_url_include and basename() each actually stop?

level: seniorimportance: should knowfreq 40%

basics

~20 s

include/require runs the file at the path you give, so request input selects the executed file: a planted local file runs as PHP, or a remote URL runs if allow_url_include is on. allow_url_include (default Off) blocks only the URL case. Use an allow-list, not basename().

open as a page

In PHP, how can comparing a hash with == (or in_array without strict) let an attacker bypass a check, and what is the fix?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Some md5/sha1 outputs are '0e' followed by digits; under == PHP treats two such hashes as the number 0, so different inputs compare equal — a 'magic hash' bypass PHP 8.0 did not change. Compare with hash_equals(), or === and in_array(..., true).

open as a page

In PHP, why is extract($_GET) or a variable variable built from request keys dangerous, and how should you read request values instead?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

extract($_GET) creates a local variable for every request key, so a request can define or overwrite variables your logic trusts (like $isAdmin). The default EXTR_OVERWRITE clobbers existing ones; variable variables ($$key) share the flaw. Read expected keys by name instead.

open as a page