Protecting Applications: Secure-by-Design, Input Validation, and Least Privilege
60 min · 5.5
Objective
Students will apply secure-by-design principles by (1) rewriting a vulnerable input handler to use server-side allowlist validation, and (2) configuring Linux file permissions with chmod to enforce least privilege, then justifying each control against a specific attack from Topic 5.1.
Hook
5 minOpen with the 2017 Equifax breach as a shift-left cautionary tale: 147 million records exposed because a known Apache Struts vulnerability (CVE-2017-5638) — a server-side input parsing bug — was not patched, and the compromised web server had broad access to internal databases (no least privilege). Ask students: 'If Equifax had followed secure-by-design and least privilege, which two decisions would have blunted this breach?' Take 2–3 responses. Steer toward: (1) server-side input validation on the Struts parser input, and (2) the web-facing process should not have had direct read access to bulk PII databases. Bridge: 'Today we build the two controls that would have mattered — before the attack, not after.'
Direct instruction
- 7m
Secure-by-Design and Shift-Left
Content
Secure-by-design means treating security as a functional requirement from the first design meeting, not a QA step at the end. The shift-left timeline compares two lifecycles: in the traditional model, security testing happens only after code freeze, so vulnerabilities discovered late are expensive (change orders, redesign, emergency patches) and often shipped anyway under deadline pressure. In the shift-left model, threat modeling happens at requirements, secure coding standards apply during development, static analysis runs on every commit, and penetration testing happens before release — so defects are caught when they cost the least to fix. Security by default is the companion idea for configuration: a database ships with authentication required, a cloud storage bucket ships private, an admin panel ships bound to localhost. IBM's Cost of a Data Breach data consistently shows a fix in design costs roughly 1× while the same fix post-release costs 30–100× — that ratio is the entire economic argument for shifting left.
Delivery
Emphasize the cost curve — students remember dollar figures. Ask: 'Name a default setting on a device you own that is insecure out of the box.' Expect answers like default Wi-Fi passwords, open Bluetooth pairing, telemetry on by default. Pre-empt the misconception that security can be bolted on later: state directly that architectural decisions (which service talks to which database, where trust boundaries live) are nearly impossible to reverse after launch — that is why design-phase decisions dominate. Targets Skill Category 1 Analyze Risk.
- 8m
Input Validation: Server-Side Allowlists
Content
Every untrusted input — form fields, URL parameters, headers, cookies, uploaded filenames, API payloads — is a potential injection vector. Input validation is a gate that filters input before it reaches application logic. Two design choices matter. First, allowlist vs. blocklist: an allowlist accepts only what matches a strict specification (e.g., 'username must be 3–20 chars matching ^[a-zA-Z0-9_]+$'); a blocklist tries to enumerate bad patterns and always misses one — attackers only need to find the miss. Allowlists fail closed; blocklists fail open. Second, client-side vs. server-side: JavaScript validation in the browser is a UX convenience only. An attacker uses curl, Burp Suite, or Postman to send raw HTTP requests directly to the server, bypassing the browser entirely. Therefore validation must live on the server, even if you also validate on the client. Worked example — a login form takes a username. Vulnerable code: query = "SELECT * FROM users WHERE name='" + input + "'". Attacker sends: ' OR '1'='1. Result: SQL injection, whole table returned. Fix: (1) server-side allowlist regex ^[a-zA-Z0-9_]{3,20}$ rejects the input outright because of the quote and space; (2) defense in depth — also use a parameterized query so even if validation misses something, the database treats input as data, not code.
Delivery
Draw the connection back to Topic 5.1 attacks — SQL injection, XSS, command injection all depend on the application trusting input. Ask: 'Why isn't blocking the word SELECT enough?' Expect answers about case variations, encoding, comments (SEL/**/ECT), Unicode. Land the point: enumerating bad is infinite; specifying good is finite. Pre-empt the misconception that client-side validation is sufficient — demo mentally: 'I open DevTools, delete the JavaScript, submit anyway. What stops me? Only the server.' Targets Skill Category 2 Mitigate Risk.
- 7m
Least Privilege and Linux File Permissions
Content
Least privilege says every process runs with the minimum rights it needs. If a compromised web server runs as root, the attacker owns the machine; if it runs as www-data with read-only access to its own directory, the blast radius is one directory. On Linux, permissions are enforced with three bits — read (r=4), write (w=2), execute (x=1) — assigned to three classes: owner, group, other. The rwx string 'rw-r-----' means owner can read and write, group can read, other has nothing; numerically that's 6-4-0 = 640. Common patterns: 600 (owner rw only — SSH private keys, secrets), 640 (owner rw, group r — config files readable by an app's group), 644 (world-readable — public web content), 755 (executable/traversable directory), 700 (private directory), 777 (everyone can do everything — almost always wrong). Command form: chmod 640 config.yml. Worked example: an app config file contains a database password. Wrong: chmod 777 config.yml — any local user reads the password. Right: chown appuser:appgroup config.yml then chmod 640 config.yml — only appuser writes, only appgroup members read, everyone else gets 'Permission denied.' This is the file-system enforcement of least privilege.
Delivery
Walk the rwx-to-numeric conversion once out loud: 'r is 4, w is 2, x is 1; add them per class.' Then quiz cold: 'What's chmod 750?' (owner rwx, group r-x, other none — 'rwxr-x---'). Pre-empt the misconception that running as root makes programs 'work better' — root doesn't add capability, it removes safety rails; a bug in a root process becomes a system compromise. Also flag that 777 is almost never the right answer on the AP exam; if a student writes 777 they should assume it is wrong. Targets Skill Category 2 Mitigate Risk.
Activities
- 32m
Harden the Vulnerable App: Input Validation + chmod LabLab
Students work in pairs on a lab machine. They (Part 1) rewrite a vulnerable Python login handler to add server-side allowlist validation and a parameterized query, then attack it with the same payloads that broke the original; (Part 2) set correct Linux permissions on the app's files using chmod and justify each choice against least privilege. Circulate and check: are they testing bypasses with curl (not just the browser)? Are they writing 640 for config.yml, not 644 or 777? At the 25-minute mark, call time and have two pairs share their permission table on the doc cam. Targets Skill Category 2 Mitigate Risk and Skill Category 1 Analyze Risk (Part 2 justification). Student handout — Harden the Vulnerable App Setup (2 min). Create a working folder and paste the two starter files below into it. Starter file 1 — login.py (VULNERABLE — do not run against real data): - import sqlite3 - def login(username, password): - conn = sqlite3.connect('users.db') - cur = conn.cursor() - query = "SELECT * FROM users WHERE name='" + username + "' AND pw='" + password + "'" - cur.execute(query) - return cur.fetchone() Starter file 2 — config.yml: - db_host: localhost - db_user: appuser - db_password: S3cret!PlzChange - debug: false Part 1 — Fix the input handler (15 min). 1. Identify every place login.py trusts user input. Write the injection payload that breaks it: username = ______ , password = ______ 2. Rewrite login.py so that BOTH of the following are true: - Server-side allowlist validation using a regex. Username must match ^[a-zA-Z0-9_]{3,20}$. Password must be 8–64 chars, printable ASCII, no whitespace. - The SQL uses a parameterized query (the ? placeholder with a tuple), not string concatenation. 3. Test your fix. From the terminal, call your function with each of these inputs and record PASS (rejected or safely handled) or FAIL: - username = `alice`, password = `Password123` → expected: normal lookup - username = `' OR '1'='1`, password = `x` → expected: rejected by validator - username = `<script>alert(1)</script>`, password = `x` → expected: rejected - username = `admin`, password = `' OR '1'='1' --` → expected: rejected - username = `bob`, password = ` ` (single space) → expected: rejected (whitespace) 4. Client-side JavaScript validation does NOT count as a fix for this activity. Explain in one sentence why not: ______________________________________________________________ Part 2 — Set least-privilege file permissions (12 min). Assume the app runs as user `appuser` in group `appgroup`. A separate user `attacker` also exists on the machine and is NOT in appgroup. Fill in the chmod value and one-sentence justification for each file: - login.py — owned by appuser:appgroup — chmod ______ — justify: ______ - config.yml (contains the DB password) — owned by appuser:appgroup — chmod ______ — justify: ______ - users.db — owned by appuser:appgroup — chmod ______ — justify: ______ - /var/log/app/ (log directory the app writes to) — owned by appuser:appgroup — chmod ______ — justify: ______ - /var/www/public/ (static site files served to the world) — chmod ______ — justify: ______ Then run `ls -l` and paste the resulting permission strings next to your numeric answers to verify. Part 3 — Analyze risk (3 min, exit ticket for the activity). Name one control from Part 1 and one from Part 2, and for each write one sentence describing which Topic 5.1 attack it stops and how.
Materials
- Student lab computer with Linux terminal (or WSL / VM / online Linux sandbox such as https://bellard.org/jslinux/)
- Text editor (VS Code, nano, or vim)
- Python 3 installed
- Provided starter files: login.py and config.yml (contents shown in the handout below)
Example outputs
- Part 1 fixed code (excerpt): import re; USERNAME_RE = re.compile(r'^[a-zA-Z0-9_]{3,20}$'); if not USERNAME_RE.fullmatch(username): raise ValueError('invalid username'); cur.execute('SELECT * FROM users WHERE name=? AND pw=?', (username, password))
- Part 1 Q4 answer: An attacker sends HTTP requests directly with curl or Burp Suite and never runs the browser JavaScript, so client-side checks are bypassed entirely — only the server sees every request.
- Part 2 permission table: login.py → 750 (appuser executes/edits, group can run, other blocked); config.yml → 640 (only appuser writes the DB password, group reads, world cannot read the secret); users.db → 600 (only appuser touches the database file); /var/log/app/ → 750 (app writes, group can list, other blocked); /var/www/public/ → 755 (world-readable static content, only owner writes).
- Part 3 analysis: The allowlist regex stops SQL injection because the payload ' OR '1'='1 contains characters not in [a-zA-Z0-9_] and is rejected before touching the database. chmod 600 on users.db enforces least privilege because even if the attacker user compromises another service on the box, they cannot read the users table.
- Common wrong answer to catch: student writes chmod 777 config.yml — the DB password is now world-readable; this is the opposite of least privilege.
- Common wrong answer to catch: student only adds JS validation in the browser and claims Part 1 is done — remind them curl bypasses the browser.
- presentation_text_missing
No-equipment fallback
If terminals are unavailable, run this as a paper lab: print the starter files, have students write the rewritten login.py by hand and the chmod table on the handout, then walk the class through executing 'ls -l' output using the diagram values.
Formative assessment
8 minA developer adds JavaScript in the browser that blocks any username containing a single quote before the form is submitted. The application performs no other checks on the username. Which statement best evaluates this control? A) It is sufficient because SQL injection requires a single quote. B) It is insufficient because an attacker can submit HTTP requests directly with a tool like curl, bypassing the browser. C) It is sufficient because modern browsers enforce the check. D) It is insufficient because JavaScript cannot detect single quotes.
multiple choiceB. Client-side validation is bypassed by any tool that sends raw HTTP (curl, Burp, Postman); the server never enforced the rule. Targets Skill Category 1 Analyze Risk.You are auditing a Linux server. The file /etc/app/secrets.yml stores an API key and is currently set to permissions 666 (rw-rw-rw-), owned by root:root. The app itself runs as the user 'appsvc' (not in the root group). In 2–4 sentences, (a) state the specific risk this creates, (b) give the exact chmod and chown commands to fix it, and (c) justify the new permissions against least privilege.
short answer(a) Any local user on the machine can read AND write the API key — a compromise of any account, or a malicious script running as any user, leaks or modifies the secret. (b) chown appsvc:appsvc /etc/app/secrets.yml then chmod 600 /etc/app/secrets.yml. (c) 600 means only appsvc can read or write the file; no group access is needed because no other process legitimately needs the key, and 'other' gets nothing — the minimum rights required for the app to function. Targets Skill Category 2 Mitigate Risk.Convert the following rwx string to its numeric chmod value and identify one file for which it would be an appropriate least-privilege setting: rwxr-x---
calculation750. Owner rwx = 4+2+1 = 7; group r-x = 4+0+1 = 5; other --- = 0. Appropriate for an executable script or program directory that the owning user runs and edits, that members of the app's group may execute or traverse, and that all other users must not touch — e.g., /opt/myapp/bin/run.sh.A team argues: 'We'll add input validation in the last sprint before release — right now we need to ship features.' Using the shift-left principle and one concrete example, explain in 2–3 sentences why this plan increases risk and cost.
short answerDelaying validation means vulnerabilities are baked into the architecture — for example, if the app already concatenates user input into SQL strings across 40 endpoints, retrofitting parameterized queries and allowlists costs 30–100× more than designing them in from the start, and every day before the fix the app is exploitable in production. Secure-by-design treats validation as a requirement, not a late feature. Targets Skill Category 1 Analyze Risk.
Vocabulary
- secure by design
- An engineering approach where security requirements and threat modeling are built into every phase of development from the first design decisions, not added after the code is written.
- security by default
- A configuration philosophy where a system ships with its most restrictive, safest settings enabled — users must deliberately opt in to reduce security, not opt in to gain it.
- input validation
- The process of checking that data entering a program conforms to expected type, length, format, and range before it is used, performed on the server side because the client is untrusted.
- sanitization
- Transforming input to neutralize dangerous content (e.g., escaping <, >, ', quotes) so it cannot be interpreted as code by a downstream parser like SQL or HTML.
- allowlist
- A validation strategy that accepts only inputs matching a defined set of known-good values or a strict pattern; everything else is rejected. Contrast with a blocklist, which fails open.
- hardening
- Systematically reducing a system's attack surface by disabling unused services, removing default accounts, applying patches, and tightening configurations.
- least privilege
- Granting a process, user, or account only the minimum permissions required to perform its function, so a compromise limits, rather than expands, the attacker's reach.
- file permissions (rwx)
- On Linux, three permission bits — read (r=4), write (w=2), execute (x=1) — assigned to three classes: owner, group, and other.
- chmod
- The Linux command that changes file permission bits; e.g., chmod 640 file.txt gives owner rw, group r, other none.
- attack surface reduction
- The practice of minimizing the number of entry points (open ports, running services, exposed endpoints, unused features) an attacker can target.
- defense in depth
- Layering multiple independent controls so that if one fails (e.g., client-side validation is bypassed), another (server-side validation) still stops the attack.
Common misconceptions
- 'Client-side JavaScript validation is enough.' Wrong — attackers send raw HTTP with curl, Burp Suite, or Postman and never execute the browser JavaScript, so only server-side validation is a real control.
- 'Running the app as root makes it work better / avoids permission errors.' Wrong — root does not add capability, it removes safety rails; a bug or RCE in a root process becomes a full system compromise, whereas the same bug in a least-privileged process is contained.
- 'Default settings are safe because the vendor set them.' Wrong — many systems ship with open ports, default admin/admin credentials, verbose error pages, and world-readable files; security-by-default is an aspiration, not a guarantee, so hardening is required on every deploy.
- 'A blocklist of dangerous strings (SELECT, <script>, ../) is equivalent to an allowlist.' Wrong — blocklists always miss encodings, case variants, comments, and Unicode homoglyphs (e.g., SEL/**/ECT, %3Cscript%3E); allowlists specify the finite good set and fail closed.
- 'Security can be added at the end of development.' Wrong — architectural trust boundaries and data-flow decisions are extremely expensive to reverse after launch; IBM data shows post-release fixes cost 30–100× more than design-phase fixes.
Materials checklist
- Lab computers with Linux terminal access (native, WSL, VM, or online sandbox)
- Python 3 with the sqlite3 module (standard library)
- Text editor for each student pair
- Printed or digital copy of the student handout with starter files login.py and config.yml
- Doc cam or projector share for pair demonstrations
- Formative assessment printed or in the LMS