Filter concepts by levelShowing all levels.

Python · Web security

Security-related web concepts

CORS, CSRF, XSS, and HTTPS/TLS/same-origin policy — the browser-enforced and transport-layer defenses a backend engineer must design around

  • CORSbrowser-enforced — reflect a specific origin, never "*" with credentials
  • CSRF tokena forging page can trigger a request but cannot read the token to include
  • Escape by contextXSS defense means escaping for text vs. attribute vs. URL, not just chars
  • TLS ≠ app securityencrypts transit and proves identity — does nothing against XSS/CSRF

1.CORS (Cross-Origin Resource Sharing)

CORS is a browser-enforced relaxation of the same-origin policy — by default a browser blocks a page on one origin from reading a response from a different origin, and CORS is the set of response headers a server can send to explicitly allow specific other origins to read its responses.

Key features

  • CORS is enforced by the BROWSER, not the server — curl or a Python script ignores it entirely; only a page running in a browser is restricted
  • Access-Control-Allow-Origin on the response is the core header — either a specific origin or "*" (any origin, no credentials allowed)
  • "Simple" requests (GET/POST with plain form content types) go straight through; other requests trigger a preflight OPTIONS request first
  • A preflight OPTIONS request asks "would you accept this?" before the real request is sent — the server answers with Access-Control-Allow-Methods/-Headers
  • Access-Control-Allow-Credentials: true is required (alongside a SPECIFIC origin, never "*") for a cross-origin request to send/receive cookies

Key CORS response headers

Key CORS response headers
HeaderPurpose
Access-Control-Allow-OriginWhich origin(s) may read this response
Access-Control-Allow-MethodsWhich HTTP methods are allowed cross-origin
Access-Control-Allow-HeadersWhich request headers the client is allowed to send
Access-Control-Allow-CredentialsWhether cookies/credentials may be included (requires a specific origin, not "*")
The browser enforces CORS — the server only sets headers
Page JS
Browser
Server
  1. 1fetch("https://api.example.com/data")
  2. 2GET /data (Origin: https://app.example.com)
  3. 3Access-Control-Allow-Origin: https://app.example.comorigin is on the server's allowlist
  4. 4response readable
  5. 5blocked — no matching headerthe HTTP request still reached the server either way
  1. Page JS → Browser: fetch("https://api.example.com/data")
  2. Browser → Server: GET /data (Origin: https://app.example.com)
  3. Server → Browser: Access-Control-Allow-Origin: https://app.example.com (origin is on the server's allowlist)
  4. Browser → Page JS: response readable
  5. Browser → Page JS: blocked — no matching header (the HTTP request still reached the server either way)

Command

python
# a minimal, framework-agnostic CORS response for one specific origin
def cors_headers(origin: str) -> dict:
    allowed = {'https://app.example.com'}
    if origin not in allowed:
        return {}
    return {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'GET, POST',
        'Access-Control-Allow-Credentials': 'true',
    }

CORS is enforced by the browser, not the server — reflect a specific validated origin (never "*") whenever credentials are involved.

2.CSRF (Cross-Site Request Forgery)

CSRF tricks a logged-in user's own browser into sending a real, authenticated request to a site they are logged into — the browser automatically attaches the session cookie, so the request looks completely legitimate to the server even though the user never intended to make it.

Key features

  • CSRF exploits that a browser automatically attaches cookies to ANY request to that domain — including one initiated by a hidden form on an unrelated malicious page
  • A CSRF token is a random, per-session (or per-form) value the server requires in the request BODY — an attacker's page cannot read it to include it, since reading requires a cross-origin fetch the same-origin policy blocks
  • SameSite=Lax/Strict cookies are a complementary defense: the browser itself withholds the cookie on many cross-site requests, independent of any token
  • CSRF specifically targets STATE-CHANGING requests (POST/PUT/DELETE) — a GET should never change state, so CSRF on a pure GET is a design bug, not just a missing token
  • secrets.compare_digest() (not ==) should compare a submitted CSRF token against the expected one, to avoid a timing side-channel
Why an attacker's page can trigger the request but not read the token
Evil page
Browser
Bank server
  1. 1hidden form auto-submits to bank.com
  2. 2POST /transfer (Cookie: session_id=... attached automatically)browser sends the cookie regardless of which page triggered it
  3. 3403 — CSRF token missing or wrongthe evil page never had access to the real token to include it
  1. Evil page → Browser: hidden form auto-submits to bank.com
  2. Browser → Bank server: POST /transfer (Cookie: session_id=... attached automatically) (browser sends the cookie regardless of which page triggered it)
  3. Bank server → Browser: 403 — CSRF token missing or wrong (the evil page never had access to the real token to include it)

Command

python
import secrets

def generate_csrf_token() -> str:
    return secrets.token_urlsafe(32)

def verify_csrf_token(submitted: str, expected: str) -> bool:
    return secrets.compare_digest(submitted, expected)   # constant-time, not ==

A CSRF token in the request body defeats forgery because a forging page cannot read it — and state-changing actions must never live behind a plain GET.

3.XSS (Cross-Site Scripting)

XSS happens when untrusted input (a username, a comment, a URL parameter) ends up rendered into a page as if it were trusted HTML/JavaScript instead of plain text — the browser then executes it exactly as if the site's own developers had written it.

Key features

  • Reflected XSS: malicious input comes from the current request (a URL parameter) and is echoed straight back into the response
  • Stored XSS: malicious input is saved (a comment, a profile field) and rendered later to OTHER users who never submitted anything
  • DOM-based XSS: the vulnerability is entirely client-side JavaScript writing untrusted data into the DOM, never touching the server at all
  • HTML-escaping untrusted output (converting < > & " ' to entities) is the core defense — every major template engine (Jinja2, Django templates) auto-escapes by default
  • A Content-Security-Policy header is defense in depth: it can block inline <script> execution even if an escaping bug slips through
Escaping turns a live tag into inert text, before interpolation
no escaping

user_comment

<script>steal cookie</script>

html.escape(user_comment)

f"<p>{user_comment}</p>"

skipped escaping — browser runs the script

f"<p>{escaped}</p>"

&lt;script&gt;... — rendered as visible text

  • user_comment — <script>steal cookie</script>
    • on error, leads to f"<p>{user_comment}</p>" (no escaping)
    • leads to html.escape(user_comment)
  • html.escape(user_comment)
    • leads to f"<p>{escaped}</p>"
  • f"<p>{user_comment}</p>" — skipped escaping — browser runs the script
  • f"<p>{escaped}</p>" — &lt;script&gt;... — rendered as visible text

Command

python
import html

user_input = '<script>alert(1)</script>'
safe = html.escape(user_input)
print(safe)   # &lt;script&gt;alert(1)&lt;/script&gt; -- inert text, not a tag

Escape untrusted output for the CONTEXT it lands in (text, attribute, URL) — escaping characters is not the same as validating meaning.

4.HTTPS, TLS, and the same-origin policy

HTTPS is HTTP carried over TLS, which encrypts and authenticates the connection so a network observer cannot read or silently modify traffic; the same-origin policy is a separate, browser-enforced rule that a page from one origin (scheme + host + port) cannot read data from a different origin by default.

Key features

  • TLS provides three things: encryption (contents unreadable in transit), integrity (tampering is detectable), and authentication (the certificate proves you are talking to the real server)
  • An origin is the exact triple scheme + host + port — https://api.example.com and http://api.example.com are DIFFERENT origins (scheme differs), as are :443 and :8443 (port differs)
  • The same-origin policy blocks a script on one origin from reading (not necessarily sending) a response from another origin — CORS is the controlled, opt-in exception to this
  • HTTPS protects data IN TRANSIT only — it does nothing to stop XSS, SQL injection, or a CSRF-forged request, all of which happen inside an already-encrypted connection
  • HSTS (Strict-Transport-Security header) tells a browser to never attempt plain HTTP to this host again for a set duration, closing the window for an initial-request downgrade attack

What counts as the "same" origin

What counts as the "same" origin
URL AURL BSame origin?
https://example.com/ahttps://example.com/bYes — path does not matter
https://example.comhttp://example.comNo — scheme differs
https://example.comhttps://api.example.comNo — host differs (subdomain)
https://example.com:443https://example.com:8443No — port differs
TLS vs. the same-origin policy — two separate protections

TLS (HTTPS)

  • Encrypts and authenticates data IN TRANSIT
  • Proves the server is who it claims to be
  • Does nothing against XSS, CSRF, or SQL injection

Same-origin policy

  • Browser rule: scheme + host + port must all match
  • Blocks one origin's script from reading another's response
  • CORS is the controlled, opt-in exception to it
  • TLS (HTTPS)
    • Encrypts and authenticates data IN TRANSIT
    • Proves the server is who it claims to be
    • Does nothing against XSS, CSRF, or SQL injection
  • Same-origin policy
    • Browser rule: scheme + host + port must all match
    • Blocks one origin's script from reading another's response
    • CORS is the controlled, opt-in exception to it

Command

python
import ssl, httpx

with httpx.Client(verify=True) as client:   # verify=True is the default -- validates the cert chain
    response = client.get('https://api.example.com/data')
    # a self-signed or expired cert here raises httpx.ConnectError, connection refused before any data moves

TLS protects data in transit and proves server identity — it does nothing against XSS/CSRF/injection, and the same-origin policy (not TLS) is what isolates one site's data from another's script.

Remember

  • CORS is enforced by the browser, not the server — curl/scripts ignore it entirely.
  • A CSRF token in the request body defeats forgery because a forging page cannot read it to include it.
  • Escape untrusted output for the CONTEXT it lands in — a URL needs scheme validation, not just character escaping.
  • TLS protects data in transit and proves server identity — it does not stop XSS, injection, or a forged request.

Pro tip

  • Access-Control-Allow-Origin: "*" combined with Allow-Credentials: true is spec-invalid — browsers reject it outright.
  • State-changing actions must never live behind a plain GET — a browser sends GET automatically for embedded resources.
  • Use secrets.compare_digest, never ==, to compare a submitted token against the expected one.
  • Never disable certificate verification (verify=False) to silence a local TLS error — fix the actual CA/cert issue instead.

Next up

  • API Development

Sources

  • OWASP — owasp.org/www-community/attacks
  • MDN Security — developer.mozilla.org/en-US/docs/Web/Security
  • Token/escaping code run for real on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.