Challenge - Secure architecture
Build a hardened API with rate limiting, CSP and session_regenerate_id.
Hardened API architecture
In this lesson you harden the API from four sides. Rate limiting stops a client from sending too many requests; RateLimiter keeps a counter per key and rejects when the limit is exceeded. CSP restricts content sources with the Content-Security-Policy header.
session_regenerate_id(true) renews the session id; it is used after login against session fixation. password_needs_rehash detects old bcrypt hashes and re-hashes when needed. filter_var(..., FILTER_VALIDATE_EMAIL) is the bedrock of input validation.
Exercise Write the security layer
The starting code has the ETL pipeline, ReflectionSerializer, Cache, benchmark and ReportService ready. Now add five pieces. The RateLimiter class: private array $counts = [], promoted private int $limit = 3; allow(string $key): bool increments the counter and returns true while $limit is not exceeded.
applyCsp(): void sends header("Content-Security-Policy: default-src 'self'"). SessionGuard::regenerate(): void calls session_regenerate_id(true). needsRehash(string $hash): bool returns password_needs_rehash($hash, PASSWORD_DEFAULT). validateEmail(string $email): bool returns filter_var(..., FILTER_VALIDATE_EMAIL) !== false. The demo makes three requests with RateLimiter(2) and prints two email and two hash cases.
Show example solution
bool increments a counter and returns whether the limit is still satisfied. applyCsp(): void sends Content-Security-Policy via header(). SessionGuard::regenerate() calls session_regenerate_id(true). needsRehash(string $hash): bool uses password_needs_rehash to detect old hashes. validateEmail(string $email): bool uses filter_var(..., FILTER_VALIDATE_EMAIL).
Example solution:
<?php
// ETL + Reflection + Performance + Security: write RateLimiter + CSP + SessionGuard
function readRows(array $rows): Generator {
foreach ($rows as $row) {
yield $row;
}
}
function transform(iterable $rows, callable $fn): Generator {
foreach ($rows as $row) {
yield $fn($row);
}
}
function pipeline(array $rows, callable $fn): Generator {
yield from transform(readRows($rows), $fn);
}
function loadAll(iterable $rows): array {
$out = [];
foreach ($rows as $row) {
$out[] = $row;
}
return $out;
}
#[Attribute]
class Field {
public function __construct(public string $name = "") {}
}
class Product {
public function __construct(
#[Field("id")] public int $id,
#[Field("name")] public string $name,
) {}
}
class ReflectionSerializer {
public function serialize(object $obj): array {
$rc = new ReflectionClass($obj);
$out = [];
foreach ($rc->getProperties() as $prop) {
$attrs = $prop->getAttributes(Field::class);
if (empty($attrs)) continue;
$field = $attrs[0]->newInstance();
$key = $field->name !== "" ? $field->name : $prop->getName();
$out[$key] = $prop->getValue($obj);
}
return $out;
}
}
class Cache {
private array $store = [];
public function get(string $key, callable $loader) {
if (!isset($this->store[$key])) {
$this->store[$key] = $loader();
}
return $this->store[$key];
}
}
function benchmark(callable $fn): array {
$start = hrtime(true);
$result = $fn();
$elapsed = (int)((hrtime(true) - $start) / 1000000);
$mem = memory_get_usage(true);
return ["result" => $result, "ms" => $elapsed, "bytes" => $mem];
}
class ReportService {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->pdo->exec("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category_id INTEGER)");
$this->pdo->exec("CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT)");
$this->pdo->prepare("INSERT INTO categories (name) VALUES (?)")->execute(["kirtasiye"]);
$this->pdo->prepare("INSERT INTO products (name, category_id) VALUES (?, ?)")->execute(["kale", 1]);
$this->pdo->prepare("INSERT INTO products (name, category_id) VALUES (?, ?)")->execute(["defter", 1]);
}
public function eagerReport(): array {
$sql = "SELECT p.name AS product, c.name AS category FROM products p JOIN categories c ON p.category_id = c.id ORDER BY p.id";
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
}
class RateLimiter {
private array $counts = [];
public function __construct(private int $limit = 3) {}
public function allow(string $key): bool {
$this->counts[$key] = ($this->counts[$key] ?? 0) + 1;
return $this->counts[$key] <= $this->limit;
}
}
function applyCsp(): void {
header("Content-Security-Policy: default-src 'self'");
}
class SessionGuard {
public function regenerate(): void {
session_regenerate_id(true);
}
}
function needsRehash(string $hash): bool {
return password_needs_rehash($hash, PASSWORD_DEFAULT);
}
function validateEmail(string $email): bool {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// Demo:
$rl = new RateLimiter(2);
echo "req1: " . ($rl->allow("ip1") ? "ok" : "blocked") . "\n";
echo "req2: " . ($rl->allow("ip1") ? "ok" : "blocked") . "\n";
echo "req3: " . ($rl->allow("ip1") ? "ok" : "blocked") . "\n";
echo "email_valid: " . (validateEmail("ayse@site.com") ? "yes" : "no") . "\n";
echo "email_invalid: " . (validateEmail("not-email") ? "yes" : "no") . "\n";
echo "rehash_md5: " . (needsRehash(md5("x")) ? "1" : "0") . "\n";
echo "rehash_fresh: " . (needsRehash(password_hash("x", PASSWORD_DEFAULT)) ? "1" : "0") . "\n";
applyCsp();
echo "csp: applied\n";
Hardening recap
In this lesson you set up four defense lines. RateLimiter keeps a per-client counter; it returns false when the limit is exceeded. applyCsp adds the default-src 'self' restriction with a single header() call. SessionGuard::regenerate produces a new id against session fixation.
needsRehash detects old hashes when bcrypt parameters change; validateEmail sets the foundation of input validation with filter_var. The next lesson merges all these layers with modern PHP features: enum, readonly, match and Fiber.
Test yourself
-
What does rate limiting prevent?
- SQL injection attacks
- XSS attacks
- CSRF attacks
- A client sending too many requests
Show answer
A client sending too many requests — Correct. Rate limiting stops a client from sending too many requests; it provides brute-force and flood protection.
-
Complete the filter_var call
$ok = ____($email, FILTER_VALIDATE_EMAIL) !== false;Show answer
$ok = filter_var($email, FILTER_VALIDATE_EMAIL) !== false; -
Match security concepts
- RateLimiter
- Blocks excessive requests
- CSP
- Restricts content sources
- session_regenerate_id
- Renews session id
- filter_var
- Validates input