diff options
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | docker/nginx/default.conf | 8 | ||||
| -rw-r--r-- | src/app/bootstrap.php | 11 |
3 files changed, 45 insertions, 0 deletions
@@ -154,6 +154,32 @@ A session fixation attack works like this: the attacker visits the site and gets `session_regenerate_id(true)` prevents this by generating a brand-new session ID the moment a user logs in. The old ID (`abc123`) is destroyed and becomes useless. Even if the attacker planted it, they can't use it after the victim authenticates — the real session now lives under a new, unknown ID. +### Session cookie hardening + +The session ID is stored in a cookie, and by default PHP sends that cookie with no special flags — meaning JavaScript can read it, it's sent over plain HTTP, and it's attached to cross-origin requests. Each of these opens an attack surface. Camagru configures three flags via `session_set_cookie_params()` in `bootstrap.php`: + +- **`httponly`** — Tells the browser that this cookie is off-limits to JavaScript. Without this flag, a successful XSS attack (e.g. injected `<script>` in a comment) could run `document.cookie` to steal the session ID and send it to the attacker's server. With `httponly`, the cookie still gets sent with HTTP requests normally, but `document.cookie` simply won't include it — so even if XSS executes, it can't exfiltrate the session. + +- **`secure`** — Tells the browser to only send this cookie over HTTPS connections. Without this flag, if a user visits the site over plain HTTP (or an attacker downgrades the connection), the session cookie is transmitted in cleartext and can be intercepted by anyone on the same network (coffee shop Wi-Fi, for example). Since Camagru runs on HTTP in local development but might use HTTPS in production, this flag is set dynamically: the bootstrap checks `$_SERVER['HTTPS']` and the `X-Forwarded-Proto` header (set by reverse proxies like load balancers that terminate TLS) to detect whether the current request arrived over HTTPS. + +- **`samesite=Lax`** — Controls when the browser attaches the cookie to cross-origin requests. With `Strict`, the cookie is never sent on cross-origin requests — but this breaks legitimate flows like clicking a link to Camagru from an email (you'd arrive logged out). `Lax` is the practical middle ground: the cookie is sent for top-level navigations (clicking links) but *not* for cross-origin form submissions or AJAX requests. This means an attacker's site can't trigger a POST to Camagru that rides on your session — the browser simply won't attach the cookie. This complements CSRF tokens: SameSite stops the browser from sending the cookie in the first place, while CSRF tokens catch anything that gets through. + +### Security headers + +Nginx adds HTTP response headers that instruct the browser to enforce additional security policies. These are defense-in-depth — they don't fix server-side vulnerabilities, but they limit what an attacker can do if they find one: + +- **`X-Content-Type-Options: nosniff`** — Browsers sometimes try to be "helpful" by guessing the content type of a response (MIME sniffing). For example, if an attacker uploads a file that looks like HTML, the browser might render it as a web page even if the server says it's an image. `nosniff` tells the browser to trust the `Content-Type` header and never guess — a response served as `image/jpeg` will always be treated as an image. + +- **`X-Frame-Options: DENY`** — Prevents any website from embedding Camagru inside an `<iframe>`. Without this, an attacker could overlay invisible Camagru pages on top of a decoy site and trick users into clicking buttons they can't see (clickjacking). For example, the "delete post" button could be positioned exactly where the attacker's "Click to win a prize" button appears. `DENY` means no site (not even Camagru itself) can frame the page. + +- **`Content-Security-Policy`** — The most powerful header. It whitelists exactly which sources the browser is allowed to load resources from. Camagru's policy is: + - `default-src 'self'` — Only load resources (scripts, fonts, etc.) from Camagru's own origin by default + - `style-src 'self' 'unsafe-inline'` — Allow stylesheets from the same origin plus inline `style` attributes (needed for dynamic UI) + - `img-src 'self' blob: data:` — Allow images from the same origin, `blob:` URLs (webcam snapshots), and `data:` URLs (base64 previews) + - `media-src 'self' blob:` — Allow media (webcam video stream) from the same origin and `blob:` URLs + + If an attacker manages to inject HTML into the page, CSP prevents them from loading external scripts (`<script src="https://evil.com/steal.js">`) because `evil.com` is not in the whitelist. The browser simply refuses to fetch it. + ### User enumeration When a login fails, our error message says "Invalid username or password" rather than "Username not found" or "Wrong password". Similarly, the forgot-password form always says "If that email is registered, a reset link has been sent." This is deliberate: if the server told you *which* part was wrong, an attacker could probe the system to build a list of valid usernames or emails, then target those accounts specifically with brute-force or phishing attacks. diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 44ebf62..e778b44 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -4,6 +4,14 @@ server { root /var/www/html/public; index index.php; + # Prevent browsers from MIME-sniffing a response away from the declared type + add_header X-Content-Type-Options "nosniff" always; + # Block the page from being loaded inside an iframe (prevents clickjacking) + add_header X-Frame-Options "DENY" always; + # Only allow resources from the same origin — inline styles are needed for + # GD-generated image previews, media-src blob: for webcam capture + add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:;" always; + location / { try_files $uri $uri/ /index.php?$query_string; } diff --git a/src/app/bootstrap.php b/src/app/bootstrap.php index 835615b..144939b 100644 --- a/src/app/bootstrap.php +++ b/src/app/bootstrap.php @@ -3,6 +3,17 @@ declare(strict_types=1); // Application bootstrap: loads .env, registers the autoloader, and configures error reporting. +// Harden session cookie: httponly prevents JS access (mitigates XSS stealing +// the session ID), samesite=Lax blocks cross-origin form submissions while +// still allowing normal link navigation, secure ensures the cookie is only +// sent over HTTPS (automatically detected from the request) +$isHttps = ($_SERVER['HTTPS'] ?? '') === 'on' + || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; +session_set_cookie_params([ + 'httponly' => true, + 'samesite' => 'Lax', + 'secure' => $isHttps, +]); session_start(); // Load .env |
