File upload security — MIME validation, storage outside webroot
Intermediate5 min read·php-16-006
security
Concept
Input validation and sanitization are distinct but complementary:
- Validation: Verifying input meets requirements (email format, range, required fields). Reject invalid input.
- Sanitization: Transforming input to remove or escape dangerous characters. Use only as a secondary defense, never as the primary.
filter_var(mixed $value, int $filter, array|int $options = 0): PHP's built-in filter function. Two families:
- Validation filters (
FILTER_VALIDATE_*): Return the validated value on success,falseon failure. Examples:FILTER_VALIDATE_EMAIL,FILTER_VALIDATE_URL,FILTER_VALIDATE_INT,FILTER_VALIDATE_FLOAT,FILTER_VALIDATE_IP,FILTER_VALIDATE_BOOLEAN. - Sanitization filters (
FILTER_SANITIZE_*): Return cleaned input. Examples:FILTER_SANITIZE_NUMBER_INT(remove all non-numeric chars except+and-),FILTER_SANITIZE_EMAIL(remove illegal chars from email).
filter_input(int $type, string $varname, int $filter): Like filter_var but reads from superglobals (INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER) without directly accessing $_GET/$_POST — slightly safer.
Laravel validation: Use $request->validate() or Form Request objects. Validation rules are declarative, composable, and integrated with error messaging. Always prefer this over manual filter_var in Laravel applications.
What not to do:
strip_tags()alone is not XSS protection — it's incomplete.addslashes()is not SQL injection protection.- Don't sanitize at input time — sanitize at output time based on context (HTML escaping, SQL parameterization, etc.).
Code Example
php
<?php
declare(strict_types=1);
// filter_var — validation
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
throw new \InvalidArgumentException("Invalid email address");
}
// $email is now a validated email string
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 150]]);
if ($age === false) {
throw new \InvalidArgumentException("Age must be between 0 and 150");
}
$url = filter_var($_POST['website'] ?? '', FILTER_VALIDATE_URL);
$ip = filter_var($_SERVER['REMOTE_ADDR'] ?? '', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
// Boolean validation (handles "true"/"false"/"1"/"0"/"on"/"off")
$subscribe = filter_var($_POST['subscribe'] ?? '', FILTER_VALIDATE_BOOLEAN);
// filter_input — read from superglobal directly
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) ?? 1;
// Laravel validation in controller
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email:rfc,dns|unique:users,email',
'password' => 'required|string|min:8|confirmed',
'age' => 'nullable|integer|between:0,150',
'role' => 'required|in:admin,editor,viewer',
'website' => 'nullable|url|max:255',
]);
// $validated only contains declared fields (mass-assignment protection)
User::create($validated);
return response()->json(['status' => 'created'], 201);
}
}
// Laravel Form Request — extract validation to a class
class CreateUserRequest extends \Illuminate\Foundation\Http\FormRequest
{
public function authorize(): bool { return true; }
public function rules(): array
{
return [
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
];
}
public function messages(): array
{
return ['email.unique' => 'This email is already registered.'];
}
}