HTTP/HTTPS Deep Dive
Breadth layernotes-security-core-knowledge.md Also see: TCP/IP Deep Dive · TLS & Cryptography
HTTP/1.1 — Request and Response Structure
HTTP is a text-based, stateless, request-response protocol that runs over TCP.
Request
GET /search?q=security HTTP/1.1\r\n ← Request line: method, path, version
Host: www.example.com\r\n ← Required in HTTP/1.1 (enables virtual hosting)
User-Agent: Mozilla/5.0 ...\r\n
Accept: text/html,application/xhtml+xml\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: en-US,en;q=0.9\r\n
Cookie: session=abc123\r\n
Connection: keep-alive\r\n
\r\n ← Empty line separates headers from body
[body — present for POST/PUT/PATCH]Response
HTTP/1.1 200 OK\r\n ← Status line: version, code, reason phrase
Date: Mon, 01 Jan 2024 12:00:00 GMT\r\n
Server: nginx/1.24.0\r\n ← Information disclosure — hide in production
Content-Type: text/html; charset=utf-8\r\n
Content-Length: 1234\r\n
Content-Encoding: gzip\r\n
Cache-Control: max-age=3600\r\n
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict\r\n
Strict-Transport-Security: max-age=31536000; includeSubDomains\r\n
Content-Security-Policy: default-src 'self'\r\n
\r\n
[body — HTML, JSON, binary, etc.]Keep-Alive and Persistent Connections
HTTP/1.0 opened a new TCP connection per request. HTTP/1.1 reuses the TCP connection (Connection: keep-alive is the default). This avoids the 3-way handshake overhead for each request.
HTTP pipeliningsend multiple requests without waiting for responses. Rarely used because of head-of-line blocking — a slow response blocks all pipelined responses behind it.
HTTP Methods
| Method | Idempotent | Safe | Body | Typical use |
|---|---|---|---|---|
| GET | Yes | Yes | No | Retrieve resource |
| HEAD | Yes | Yes | No | GET without response body; check existence/headers |
| POST | No | No | Yes | Create resource; submit form data |
| PUT | Yes | No | Yes | Replace resource entirely |
| PATCH | No | No | Yes | Partial update |
| DELETE | Yes | No | No | Delete resource |
| OPTIONS | Yes | Yes | No | Discover allowed methods (used in CORS preflight) |
| TRACE | Yes | Yes | No | Echo request back — disable in production (XST attack) |
| CONNECT | — | No | — | Establish tunnel (used for HTTPS through HTTP proxy) |
Idempotentcalling it N times has the same effect as calling it once. DELETE is idempotent (deleting an already-deleted resource still leaves it deleted). POST is not (submitting a form twice creates two records).
Safeno side effects on the server. GET should never modify state. (Violations are a security issue — e.g. GET /logout or GET /deleteAccount.)
Memory hook"safe" = read-only, "idempotent" = repeat-safe. Safe methods don't change anything (GET, HEAD) — a crawler can hit them a million times harmlessly. Idempotent methods can change state but doing them twice equals doing them once (PUT sets a value; DELETE removes — re-deleting is still deleted). POST is neither: submit a payment form twice and you've paid twice. The security punchline: a state-changing GET (
GET /deleteAccount) is a CSRF magnet, because browsers fire GETs automatically (image tags, prefetch) — never put side effects behind GET.
HTTP Status Codes
| Code | Name | Meaning |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Resource created (POST/PUT) |
| 204 | No Content | Success, no response body (DELETE) |
| 301 | Moved Permanently | Redirect; browser caches permanently |
| 302 | Found | Temporary redirect; browser doesn't cache |
| 304 | Not Modified | Conditional GET; cached version is still fresh |
| 307 | Temporary Redirect | Temporary; preserves HTTP method (unlike 302) |
| 308 | Permanent Redirect | Permanent; preserves HTTP method (unlike 301) |
| 400 | Bad Request | Malformed syntax, invalid params |
| 401 | Unauthorized | Authentication required (misleading name) |
| 403 | Forbidden | Authenticated but not authorised |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported on this endpoint |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Generic server-side error |
| 502 | Bad Gateway | Upstream server returned invalid response |
| 503 | Service Unavailable | Server overloaded or under maintenance |
| 504 | Gateway Timeout | Upstream server timed out |
Memory hook401 = "who are you?", 403 = "no." The names are misleading (401 is literally "Unauthorized" but means unauthenticated). Remember it as: 401 = authentication missing/failed → log in and try again; 403 = authenticated fine, but you're not allowed → logging in again won't help. Bonus security note: returning 404 instead of 403 hides whether a resource even exists, reducing enumeration — useful for
/adminpaths.
Security-relevant distinctions
- 401 vs 403: 401 means "prove who you are first"; 403 means "I know who you are, but no." Returning 404 instead of 403 for unauthorised resources can reduce enumeration — but don't confuse real 404s.
- 500 with stack trace: information disclosure; return a generic error to users, log details server-side.
HTTP/2 and HTTP/3
HTTP/2
requests/responses are split into binary frames, not text
multiple requests over a single TCP connection simultaneously — no head-of-line blocking at the HTTP layer
reduces overhead from repetitive headers
server can proactively send resources the client will need (largely abandoned)
browsers only implement HTTP/2 over TLS (h2); plaintext h2c exists but is unused
HTTP/2 security notes
- HPACK compression is used in CRIME/BREACH-style attacks: if an attacker can inject content adjacent to secrets (like CSRF tokens) in compressed responses, they can infer the secret via response size changes
- HTTP/2 rapid reset (CVE-2023-44487): a vulnerability allowed sending many RST_STREAM frames to reset streams immediately after opening them, exhausting server resources (record-breaking DDoS)
HTTP/3
- Built on QUIC (UDP-based transport) instead of TCP
- Connection IDs survive IP address changes (mobile handoffs)
- No head-of-line blocking even at the transport layer (QUIC streams are independent)
- 0-RTT resumption: send data in the first packet (replay risk for non-idempotent requests)
- Always encrypted: QUIC integrates TLS 1.3 — no plaintext QUIC
HTTPS — What HTTPS Adds
HTTPS = HTTP over TLS. It provides:
message content encrypted; no eavesdropping on the path
AEAD cipher prevents tampering; MITM cannot modify requests/responses without detection
server's identity verified via its X.509 certificate signed by a trusted CA
What HTTPS does NOT hide
- The server's IP address (visible in routing)
- The SNI (Server Name Indication) — hostname sent in plaintext in TLS ClientHello (though ECH — Encrypted Client Hello — is being deployed to fix this)
- Connection timing and packet sizes (traffic analysis)
- DNS queries to look up the server's IP (unless DoH/DoT is used)
HTTP Headers — Security Reference
Request Headers (set by the client/browser)
Origin: https://app.example.com # Sent on cross-origin requests; used in CORS
Referer: https://app.example.com/page # Revealing the page that initiated the request
Authorization: Bearer eyJ... # Credentials; never send over HTTP
Cookie: session=abc; csrf_token=xyz # Credentials; should be HttpOnly+SecureResponse Headers (set by the server — your responsibility)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Force HTTPS for 1 year; include all subdomains; submit to browser preload list
# Once set, browsers will NEVER connect over HTTP — ensure HTTPS is working before enabling
# 💡 Memory hook — HSTS is a ONE-WAY door with a long memory. Once a browser sees
# this header, it refuses plain HTTP to your domain for `max-age` seconds — even
# if you change your mind. Set max-age too high before HTTPS is rock-solid (or
# add includeSubDomains when a subdomain can't do HTTPS) and you've locked users
# OUT with no quick undo. Roll it out with a SHORT max-age first, then raise it.
# `preload` is even more permanent — it's hard-coded into browsers themselves.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'
# Most powerful XSS mitigation; start with report-only mode to catch breakage
# unsafe-inline and unsafe-eval negate XSS protection — avoid
X-Frame-Options: DENY
# Prevent clickjacking (framing). Superseded by CSP frame-ancestors, but keep for IE compatibility
X-Content-Type-Options: nosniff
# Prevent MIME sniffing — browser must use declared Content-Type, not guess
# Stops browsers from executing .js served as text/plain
Referrer-Policy: strict-origin-when-cross-origin
# Cross-origin requests: send only the origin (not path/query); same-origin: full URL
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()
# Disable powerful browser features you don't use
Cross-Origin-Opener-Policy: same-origin
# Isolates browsing context; enables SharedArrayBuffer; blocks window.opener hijacking
Cross-Origin-Resource-Policy: same-origin
# Prevents cross-origin embedding of this resource (Spectre mitigations)Headers to Remove (Information Disclosure)
# Remove or replace:
Server: nginx/1.24.0 → Server: (empty or generic)
X-Powered-By: PHP/8.2.0 → remove entirely
X-AspNet-Version: 4.0.xxxxx → remove entirelyAuthentication in HTTP
Basic Auth
Authorization: Basic dXNlcjpwYXNz (base64 of "user:pass")Base64 is encoding, not encryption — trivially decoded. Only use over HTTPS. The password is sent with every request. Avoid for anything user-facing; acceptable for internal APIs with mutual TLS or behind a trusted proxy.
Bearer Token (OAuth 2.0)
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.signatureToken is presented with each request. If stolen (XSS, logging leak), the bearer can use it until expiry. Store in memory (not localStorage) and send only over HTTPS.
Digest Auth
Challenge-response; password never sent in clear. Superseded by Bearer tokens. Vulnerable to MITM (downgrade to Basic).
API Keys
X-API-Key: my-secret-api-key
# or
Authorization: ApiKey my-secret-api-keyLong-lived; should be rotated periodically. Treat as passwords — don't commit to source code. Use a secrets manager.
Cookies in Depth
Setting a Secure Cookie
Set-Cookie: session=<random-256-bit-token>;
HttpOnly; # JS cannot access — blocks XSS session theft
Secure; # HTTPS only — never sent over HTTP
SameSite=Strict; # CSRF protection — never sent on cross-site requests
Path=/; # Scope to entire site
Max-Age=3600; # Expiry in seconds (prefer over Expires — uses server-relative time)
Domain=app.example.com # Omit to restrict to exact hostname (not subdomains)__Host- and __Secure- Prefixes
# __Host- prefix: browser enforces Secure + no Domain + Path=/
Set-Cookie: __Host-session=token; Secure; Path=/; SameSite=Strict; HttpOnly
# __Secure- prefix: browser enforces Secure attribute only
Set-Cookie: __Secure-session=token; Secure; SameSite=Lax; HttpOnlyThese prefixes prevent subdomain attacks from setting cookies that apply to the parent domain.
SameSite — Detailed Behaviour
| Value | When cookie is sent |
|---|---|
Strict | Only same-site requests (same origin or navigation from same site) |
Lax | Same-site OR top-level navigation (user clicking a link) — NOT for images/iframes/fetch |
None; Secure | All requests including cross-site (required for embedded widgets, OAuth redirects) |
Default in modern browsersLax (if SameSite is not specified). This broke some legacy apps that relied on cookies being sent with all cross-site requests.
CORS in Depth
Simple Requests (no preflight)
A request is "simple" if:
- Method: GET, HEAD, or POST
- Content-Type:
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No custom headers
Simple requests are sent directly. The browser checks Access-Control-Allow-Origin in the response and exposes the response to JS only if the origin matches.
Preflighted Requests
Any request that doesn't meet the simple criteria triggers a preflight OPTIONS:
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header, Content-Type
→ Server response:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-Custom-Header, Content-Type
Access-Control-Max-Age: 86400 ← cache preflight result for 24hCORS Misconfigurations
# Vulnerable: reflect any Origin
response.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin")
response.headers["Access-Control-Allow-Credentials"] = "true"
# → Any origin can make credentialed requests → CSRF + data theft
# Vulnerable: overly broad
"Access-Control-Allow-Origin": "*.example.com" # Not valid syntax — treated as literal
"Access-Control-Allow-Origin": "*" # Safe only without credentials
# Safe: strict allowlist
ALLOWED_ORIGINS = {"https://app.example.com", "https://admin.example.com"}
origin = request.headers.get("Origin", "")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin" # Tell caches this varies by originHTTP Caching
# Server instructs client/proxy how to cache:
Cache-Control: max-age=3600 # Cache for 1 hour
Cache-Control: no-cache # Validate with server before using cached copy (ETag/Last-Modified)
Cache-Control: no-store # Never cache (sensitive data — banking, auth)
Cache-Control: private # Client-side cache only; not shared proxies
Cache-Control: public, max-age=86400 # CDN-cacheable for 24h
Cache-Control: immutable # Content will never change (fingerprinted assets)
# Conditional requests (validation):
ETag: "abc123"
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT
# Client sends on revalidation:
If-None-Match: "abc123" → server returns 304 if unchanged
If-Modified-Since: Mon, 01 Jan ... → server returns 304 if unchangedSecurity implications
Cache-Control: no-storeis critical for pages containing PII, session tokens, or sensitive data- Shared proxies/CDNs may cache user-specific responses if
Vary: CookieorCache-Control: privateis missing - Cache poisoning: if the CDN caches a response that includes attacker-controlled content (via unkeyed headers) → served to all users
Common Misconfigurations
| Misconfiguration | Risk | Fix |
|---|---|---|
Verbose Server header | Reveals server version → targeted exploit selection | Remove or genericise |
| HTTP available alongside HTTPS | Credentials sent in cleartext if user types URL without https:// | Redirect all HTTP → HTTPS + HSTS |
| Wildcard CORS with credentials | Any origin can make authenticated requests | Strict allowlist; never * + credentials |
| Missing CSP | XSS payloads can load external scripts | Implement strict CSP |
| Directory listing enabled | File enumeration | Disable in nginx/Apache config |
| Sensitive data in GET params | Logged in server logs, proxy logs, Referer headers, browser history | Use POST body for sensitive data |
| TRACE method enabled | Cross-Site Tracing (XST) — can read HttpOnly cookies via JS on some old browsers | Disable TRACE |
Interview Questions
HTTP/1.1 is text-based and largely one-request-per-connection — even with keep-alive, responses come back in order, so a slow response blocks the ones behind it (head-of-line blocking). HTTP/2 is binary-framed and multiplexes many concurrent requests over a single TCP connection with header compression, removing HTTP-layer head-of-line blocking — but because it's still on TCP, a lost packet stalls all streams at the transport layer. HTTP/3 moves onto QUIC over UDP, which makes streams truly independent so packet loss only affects the one stream, integrates TLS 1.3 (always encrypted), and uses connection IDs that survive IP changes for seamless mobile handoffs. The arc is fewer round-trips and less blocking at each step.
HTTPS is HTTP over TLS, giving three things: confidentiality so an on-path eavesdropper can't read the content, integrity so they can't tamper without detection, and authenticity so the client verifies the server's identity via its CA-signed certificate. What it does not hide: the server's IP address, the hostname in the TLS SNI field which is sent in plaintext unless Encrypted Client Hello is used, the DNS lookup unless you use DoH/DoT, and traffic-analysis metadata like packet sizes and timing. So HTTPS protects the content of the conversation, but an observer still learns who you're talking to and roughly how much.
The same-origin policy stops JavaScript from reading responses from a different origin unless that origin opts in via CORS. For "simple" requests — GET/HEAD/POST with safe content types and no custom headers — the browser sends the request with an Origin header and only exposes the response to JS if the server returns a matching Access-Control-Allow-Origin. For anything else — a custom header, a DELETE, a JSON content type — the browser first sends a preflight OPTIONS request asking "can I send this method with these headers from this origin?", and only sends the real request if the server's Access-Control-Allow-* response permits it. The key point is CORS relaxes the same-origin policy under server control; it isn't a server-side access control itself.
SameSite is a cookie attribute controlling whether the browser attaches the cookie to cross-site requests. With Strict, the cookie is only sent when the request originates from the same site, so a request triggered from an attacker's page — the essence of CSRF — won't carry the victim's session cookie and the forged action fails. It cuts CSRF at the root because CSRF depends on the browser auto-attaching cookies to cross-site requests. The trade-off is that Strict also withholds the cookie on legitimate inbound navigation, like following a link from an email, so Lax is the common default — it sends the cookie on top-level GET navigations but not on cross-site sub-resource requests or form POSTs — usually paired with CSRF tokens for state-changing actions.
HSTS is a response header that tells the browser to only ever connect to this domain over HTTPS for the max-age duration, which defeats SSL-strip downgrade attacks and accidental HTTP requests. The catch is it's effectively a one-way commitment with a long memory: once a browser sees it, it refuses plain HTTP for that whole duration, even if you change your mind. So if you set a long max-age, or add includeSubDomains, before HTTPS is fully working everywhere — including every subdomain — you can lock users out with no fast undo. The safe rollout is a short max-age first, verify everything works, then raise it; and preload, which hard-codes the policy into browsers, is even more permanent and should be a deliberate final step.
Strict-Transport-Security to force HTTPS, rolled out carefully. A strong Content-Security-Policy as the main XSS backstop — ideally nonce-based with no unsafe-inline — started in report-only mode to find breakage. X-Content-Type-Options nosniff to stop MIME sniffing. X-Frame-Options DENY or CSP frame-ancestors to prevent clickjacking. Referrer-Policy strict-origin-when-cross-origin to avoid leaking URLs. Permissions-Policy to disable browser features you don't use. The cross-origin isolation headers (COOP/CORP) for Spectre hardening. And on the cookie side, HttpOnly, Secure, and SameSite. Plus removing information-disclosure headers like Server and X-Powered-By. The framing is that these are cheap defense-in-depth layers that are table stakes.
401 Unauthorized actually means unauthenticated — the server doesn't know who you are, so authenticate and try again. 403 Forbidden means you're authenticated fine but you're not permitted to access this resource, so retrying with the same identity won't help. The naming is historically confusing, so I remember it as 401 = "who are you?" and 403 = "no." A security nuance is that some apps return 404 instead of 403 for sensitive resources so attackers can't even confirm a resource or admin path exists, reducing enumeration.
Because query strings end up in far more places than the request itself. They're written to server access logs and proxy/load-balancer logs, they're stored in browser history, they're sent in the Referer header to third-party sites the page links to or loads resources from, and they can be cached. So a session token, password, or PII in a URL leaks into logs and analytics across multiple systems where it's hard to scrub and easy to expose. Sensitive data belongs in the request body of a POST, or in a header, not in the URL.
CSP is a response header telling the browser which sources of scripts, styles, and other content are allowed to load and execute, acting as a second line of defense if an XSS payload gets injected. A strong policy forbids inline scripts and only allows scripts from trusted origins or those carrying a per-request nonce, so an injected script tag simply won't run — it isn't from an allowed source and lacks the nonce. It doesn't replace output encoding; it's defense-in-depth, and the mistake that guts it is allowing unsafe-inline. I'd deploy it report-only first to find legitimate breakage, then enforce, ideally nonce-based with strict-dynamic.
no-store tells browsers and any intermediary caches never to store the response at all — not on disk, not in memory. It matters for any response containing sensitive data: authenticated pages, anything with PII, session tokens, or financial data. Without it, a response can be cached on the device or, worse, on a shared proxy or CDN, where it might be served to another user or recovered later from a shared machine. It's distinct from no-cache, which permits storing but requires revalidation before reuse. For sensitive pages you want no-store, often alongside private and a Vary on Cookie so user-specific content is never served to the wrong person.