Security Basics
Application Security Basics
Application security is about making sure software behaves the way it's supposed to — even when someone is actively trying to make it behave differently. Most breaches today don't happen at the network layer. They happen because an application trusted input it shouldn't have, exposed data it shouldn't have, or was built on a dependency that hadn't been updated in three years.
This lesson covers
The OWASP Top 10 → Input validation and why it matters → SQL injection hands-on → Authentication flaws → Insecure dependencies → Secrets in code → Security headers → How to think about application security as a developer
The OWASP Top 10
OWASP — the Open Worldwide Application Security Project — publishes a list of the ten most critical web application security risks. It's updated every few years based on real vulnerability data from thousands of organisations. If you're building or reviewing any web application, this list is where you start.
A01 — Broken Access Control
Users accessing resources or actions they shouldn't be allowed to. The most common category in the 2021 list.
A02 — Cryptographic Failures
Sensitive data transmitted or stored without proper encryption. Passwords hashed with MD5. HTTP instead of HTTPS.
A03 — Injection
SQL, NoSQL, command, and LDAP injection. Untrusted data sent to an interpreter as part of a command or query. Covered in depth below.
A04 — Insecure Design
Security flaws baked into the architecture before a single line of code is written. Can't be fixed by implementation — requires redesign.
A05 — Security Misconfiguration
Default credentials left in place, verbose error messages, unnecessary features enabled, open S3 buckets. The most preventable category.
A06 — Vulnerable Components
Libraries, frameworks, and dependencies with known vulnerabilities. The Log4Shell vulnerability in 2021 exposed hundreds of thousands of applications through a single logging library.
A07 — Auth & Identity Failures
Weak passwords permitted, no MFA, session tokens not invalidated on logout, credential stuffing not rate-limited.
A08 — Software Integrity Failures
CI/CD pipelines that pull unverified dependencies or updates. Auto-updates without signature verification. The SolarWinds attack exploited this.
A09 — Logging & Monitoring Failures
Not logging authentication failures, not alerting on anomalous access patterns. Attackers rely on detection failures to extend dwell time.
A10 — Server-Side Request Forgery
Tricking a server into making HTTP requests to internal systems on the attacker's behalf — reaching services behind the firewall that the attacker can't reach directly.
Input validation — the root of most injection flaws
The majority of injection vulnerabilities share one root cause: the application takes data from a user and passes it directly to an interpreter — a database, a shell, an XML parser — without checking what that data actually contains. The interpreter has no way to know what's user input and what's part of the intended command. So it executes all of it.
The fix is to treat all input as untrusted by default. Validate that it matches what you expect. Sanitise anything that will be rendered or executed. Use parameterised queries so user data is never interpreted as code. This isn't complicated — but it requires discipline, because the insecure way is often the faster way to write code.
SQL injection — the classic attack
SQL injection has been in the OWASP Top 10 for over two decades. It's not new. It's still everywhere. The concept is simple: if a web application builds a SQL query by concatenating user input directly into the query string, an attacker can submit input that changes the structure of the query itself.
# -------------------------------------------------------
# VULNERABLE CODE — Never do this
# -------------------------------------------------------
import sqlite3
def get_user(username):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
# User input is concatenated directly into the query
query = "SELECT * FROM users WHERE username = '" + username + "'"
cursor.execute(query)
return cursor.fetchone()
# Normal use:
get_user("alice")
# Executes: SELECT * FROM users WHERE username = 'alice'
# Attacker input:
get_user("' OR '1'='1")
# Executes: SELECT * FROM users WHERE username = '' OR '1'='1'
# '1'='1' is always true — returns EVERY row in the users table
get_user("admin'--")
# Executes: SELECT * FROM users WHERE username = 'admin'--'
# Everything after -- is a comment — the password check is gone
# -------------------------------------------------------
# SECURE CODE — Parameterised query
# -------------------------------------------------------
def get_user_safe(username):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
# User input is passed as a parameter — never interpreted as SQL
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
return cursor.fetchone()
# Attacker input is now treated as literal data, not SQL syntax
get_user_safe("' OR '1'='1")
# Executes: SELECT * FROM users WHERE username = ''' OR ''1''=''1'
# Returns nothing — no user with that exact username exists
# Vulnerable version — attacker input ' OR '1'='1 # Query sent to database: SELECT * FROM users WHERE username = '' OR '1'='1' # Database response — returns all rows: (1, 'alice', 'alice@example.com', '$2b$12$hashed_password_1') (2, 'bob', 'bob@example.com', '$2b$12$hashed_password_2') (3, 'admin', 'admin@example.com', '$2b$12$hashed_password_3') (4, 'carlos', 'carlos@example.com', '$2b$12$hashed_password_4') # Secure version — same attacker input # Query sent to database: SELECT * FROM users WHERE username = ''' OR ''1''=''1' # Database response: None ← no user exists with that literal username string
What just happened
In the vulnerable version, the attacker's single quote breaks out of the string context and the OR '1'='1' becomes a real SQL condition — always true — dumping the entire table. In the secure version, the ? placeholder tells the database driver to treat whatever comes next as a data value, not SQL syntax. The single quote gets escaped automatically. The injection attempt becomes a harmless literal string search that returns nothing.
Insecure dependencies — the silent risk
Modern applications are built on layers of third-party libraries. A typical Node.js project might have 50 direct dependencies and 800 transitive ones — packages that your packages depend on. You wrote none of that code. You're responsible for all of it.
In December 2021, a critical vulnerability was discovered in Log4j — a Java logging library used by an enormous portion of enterprise software. The vulnerability, Log4Shell, allowed remote code execution with a single malicious string. Hundreds of thousands of applications were exposed, including systems at Apple, Amazon, Cloudflare, and countless government agencies. The library had been quietly included as a dependency in software that had nothing obviously to do with logging.
# Audit a Node.js project for known vulnerabilities
npm audit
# Audit with full detail on each vulnerability
npm audit --verbose
# Automatically fix vulnerabilities where a safe upgrade exists
npm audit fix
# For Python projects — check dependencies against known CVEs
pip install pip-audit
pip-audit
# Check a specific package version against the OSV database
pip-audit --requirement requirements.txt
# For Java/Maven projects
mvn dependency-check:check
$ npm audit # npm audit report lodash <4.17.21 Severity: high Prototype Pollution - https://npmjs.com/advisories/1523 fix available via `npm audit fix` node_modules/lodash axios <0.21.2 Severity: moderate Server-Side Request Forgery - https://npmjs.com/advisories/1672 fix available via `npm audit fix` node_modules/axios minimist <1.2.6 Severity: critical Prototype Pollution - https://npmjs.com/advisories/1179 No fix available — manual review required node_modules/minimist 3 vulnerabilities (1 moderate, 1 high, 1 critical) To address all issues possible, run: npm audit fix 1 vulnerability requires manual review. See the full report for details.
What just happened
npm audit cross-references every package in your node_modules against the npm advisory database of known CVEs. Three vulnerabilities found — one critical with no automatic fix available, meaning you need to find an alternative package or patch it manually. The severity ratings matter: critical means remote code execution or data exposure is possible. Run this before every deployment and treat a critical finding as a deployment blocker.
Secrets in code — a common and serious mistake
API keys, database passwords, private keys, and tokens hardcoded into source code are one of the most consistently found vulnerabilities in real-world applications. Developers commit them accidentally. They get pushed to public repositories. They stay there — sometimes for years — because people don't realise that deleting a file from Git doesn't remove it from the history.
The attacker's perspective
Tools like truffleHog, git-secrets, and GitLeaks scan repository histories — including every commit ever made — for patterns that match API keys, AWS credentials, private keys, and passwords. Attackers run these tools against public GitHub repositories continuously. In 2022, researchers scanning public repos found over 4,000 valid AWS access keys exposed in a single week. A key committed once and deleted in a follow-up commit is still in the Git history — permanently, unless the history is explicitly rewritten.
# Scan a repository for leaked secrets using trufflehog
pip install trufflehog --break-system-packages
trufflehog git file://. --only-verified
# Scan a specific GitHub repo
trufflehog github --repo https://github.com/yourorg/yourrepo
# Pre-commit hook — prevent secrets being committed in the first place
pip install pre-commit detect-secrets --break-system-packages
# Initialise detect-secrets baseline (scans current state)
detect-secrets scan > .secrets.baseline
# Add to .pre-commit-config.yaml:
# repos:
# - repo: https://github.com/Yelp/detect-secrets
# hooks:
# - id: detect-secrets
# args: ['--baseline', '.secrets.baseline']
# The correct way to handle secrets — environment variables
# Never hardcode. Load from environment at runtime.
import os
db_password = os.environ.get("DB_PASSWORD")
api_key = os.environ.get("API_KEY")
$ trufflehog git file://. --only-verified 🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷 Found verified result 🔑🔑 Detector Type: AWS Decoder Type: PLAIN Raw result: AKIAIOSFODNN7EXAMPLE Commit: a3f91bc2d784e1f0c92a8d3e5b6f7a891234abcd Date: 2024-03-14 09:22:11 +0000 Author: dev@example.com File: config/database.js Line: 12 Found verified result 🔑🔑 Detector Type: Stripe API Key Decoder Type: PLAIN Raw result: sk_live_4eC39HqLyjWDarjtT1zdp7dc Commit: 7c4b2e9f1a3d6e8c0b5a2f4d7e1c3b5a7d9e1f3 Date: 2024-01-08 14:41:03 +0000 Author: dev@example.com File: src/payments/stripe.js Line: 3 2 verified secrets found.
What just happened
TruffleHog found two verified secrets — an AWS access key and a live Stripe API key — buried in commits from weeks ago. "Verified" means it tested the credentials and they are still active and valid. Both need to be revoked immediately — not just deleted from the codebase, but rotated in AWS and Stripe's dashboards, because anyone who found them before you has already had access. Then check every action taken with those credentials since the commit date.
Instructor's Note
If you take one habit from this lesson, make it this: use environment variables for every secret, every time, without exception. Set up a .env file locally, add it to .gitignore before your first commit, and use a secrets manager like AWS Secrets Manager, HashiCorp Vault, or GitHub Secrets in production. The cost of doing this correctly is about ten minutes of setup. The cost of getting it wrong is a production credential sitting in a public repository being actively abused while you sleep.
Practice Questions
What type of database query prevents SQL injection by treating user input as data rather than executable SQL syntax? (two words)
What was the name of the critical 2021 vulnerability in the Log4j logging library that allowed remote code execution through a single malicious string?
Instead of hardcoding API keys and passwords in source code, where should secrets be loaded from at runtime? (two words)
Quiz
In the SQL injection example, why does the input ' OR '1'='1 return all rows from the database?
A developer accidentally commits an API key to a public GitHub repo and immediately deletes it in a follow-up commit. Why is the key still compromised?
According to the OWASP Top 10 2021, what was the most commonly found vulnerability category?
Up Next · Lesson 18
Web Security Fundamentals
XSS, CSRF, clickjacking, security headers, and how HTTPS actually protects you — the complete picture of what happens when a browser talks to a web server.