Security Basics Lesson 17 – Application Security Basics | Dataplexa
Section II · Lesson 17

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.