Application Security
- Application Security
Building security into the software, not bolting it on
Application security (AppSec) encompasses the practices, tools, and techniques used to find, fix, and prevent security vulnerabilities in software. The modern approach automates AppSec controls and embeds them into the development pipeline rather than applying them as a one-time audit.
ℹ️ The tools on this page map to when in the lifecycle they run: SAST reads source code, SCA inspects your dependencies, secrets detection scans commits, and DAST attacks the running app. None is sufficient alone — they catch different bugs at different stages. This is the build-time complement to the runtime detection on the SIEM and SOC page.
Secure Coding Practices
Secure coding means writing code that is resistant to attack by default — validating all input, using safe APIs, avoiding dangerous constructs, and applying the principle of least privilege.
OWASP Top 10
The OWASP Top 10 is the authoritative list of the most critical web application security risks, updated approximately every three to four years. The 2021 edition:
| Rank | Category | Description |
|---|---|---|
| A01 | Broken Access Control | Users can act outside their intended permissions |
| A02 | Cryptographic Failures | Sensitive data exposed due to weak or missing encryption |
| A03 | Injection | Untrusted data sent to an interpreter (SQL, OS, LDAP) |
| A04 | Insecure Design | Missing or ineffective security controls by design |
| A05 | Security Misconfiguration | Insecure default configs, missing hardening, verbose errors |
| A06 | Vulnerable and Outdated Components | Libraries/frameworks with known CVEs |
| A07 | Identification and Authentication Failures | Weak authentication, credential stuffing, session fixation |
| A08 | Software and Data Integrity Failures | Unsigned updates, insecure deserialization, CI/CD compromise |
| A09 | Security Logging and Monitoring Failures | Insufficient logging, no alerting on attacks |
| A10 | Server-Side Request Forgery (SSRF) | Server makes requests to attacker-controlled destinations |
SQL Injection Prevention
SQL injection remains the most exploited injection type. It occurs when untrusted input is concatenated into a SQL query.
import sqlite3
db = sqlite3.connect('users.db')
cursor = db.cursor()
# VULNERABLE: direct string interpolation
username = "admin' OR '1'='1"
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query) # returns all rows — authentication bypassed
# SAFE: parameterized query (prepared statement)
username = "admin' OR '1'='1"
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
# Returns nothing — the literal string is treated as data, not SQL
# SAFE with SQLAlchemy ORM
from sqlalchemy import select
from sqlalchemy.orm import Session
with Session(engine) as session:
user = session.execute(
select(User).where(User.username == username)
).scalar_one_or_none()
Parameterized queries are the only reliable defense. ORMs use parameterized queries internally. Input sanitization (blacklisting quotes, etc.) is not sufficient — use allowlists for any untrusted data that must go into a query.
Cross-Site Scripting (XSS) Prevention
XSS injects malicious scripts into web pages viewed by other users. There are three types: Stored (persisted in database), Reflected (in URL parameters), and DOM-based (client-side).
# Server-side: always HTML-encode output
import html
user_input = "<script>alert('XSS')</script>"
safe_output = html.escape(user_input)
# Produces: <script>alert('XSS')</script>
# Flask/Jinja2: auto-escaping is ON by default for .html templates
# — safe (escaped automatically)
# — UNSAFE: disables escaping, do not use with untrusted data
# Django templates also auto-escape by default
# — safe
# — UNSAFE
Content Security Policy (CSP) is a defense-in-depth HTTP header that restricts which scripts can execute:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'
Input Validation
Validate all input at the boundary between untrusted and trusted code:
import re
from pathlib import Path
# Allowlist validation (preferred over denylist)
def validate_username(username: str) -> str:
"""Accept only alphanumeric + underscore, 3-32 chars."""
if not re.fullmatch(r'[a-zA-Z0-9_]{3,32}', username):
raise ValueError(f"Invalid username: {username!r}")
return username
# Path traversal prevention
def safe_open(base_dir: str, filename: str):
"""Prevent directory traversal attacks."""
base = Path(base_dir).resolve()
target = (base / filename).resolve()
if not str(target).startswith(str(base)):
raise PermissionError(f"Path traversal detected: {filename!r}")
return target.open()
# Example: reject '../../../etc/passwd'
try:
safe_open('/var/www/uploads', '../../../etc/passwd')
except PermissionError as e:
print(e) # Path traversal detected: '../../../etc/passwd'
Secure API Design
REST and gRPC APIs require their own security controls:
from functools import wraps
from flask import Flask, request, jsonify, abort
import jwt
import time
app = Flask(__name__)
SECRET_KEY = "from-vault-not-hardcoded"
def require_jwt(f):
"""Decorator: validate JWT Bearer token on every request."""
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
abort(401, 'Missing Bearer token')
token = auth[7:]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
abort(401, 'Token expired')
except jwt.InvalidTokenError:
abort(401, 'Invalid token')
return f(payload, *args, **kwargs)
return decorated
@app.route('/api/resource/<int:resource_id>')
@require_jwt
def get_resource(payload, resource_id):
# Enforce object-level authorization (IDOR prevention)
user_id = payload['sub']
resource = db.get_resource(resource_id)
if resource is None:
abort(404)
if resource.owner_id != user_id:
abort(403, 'Forbidden') # do not reveal existence
return jsonify(resource.to_dict())
API security checklist:
- Authenticate every endpoint (no unauthenticated routes by accident)
- Enforce object-level authorization (check ownership, not just authentication)
- Rate limit to prevent brute force and abuse
- Log all requests with status codes for audit
- Use HTTPS everywhere (HSTS header)
- Validate Content-Type and reject unexpected media types
Static Application Security Testing (SAST)
SAST analyzes source code without executing it, finding vulnerabilities by examining code patterns.
Bandit (Python)
Bandit is the standard Python SAST tool, part of the PyCQA organization:
pip install bandit
# Scan a directory
bandit -r myapp/
# Report only medium and above
bandit -r myapp/ -ll
# JSON output for CI parsing
bandit -r myapp/ -f json -o bandit-report.json
# Suppress a false positive inline
result = subprocess.run(cmd, shell=False) # noqa: S603
# Run only specific tests
bandit -r myapp/ -t B301,B302,B303 # only deserialization checks
Bandit test categories relevant to DevSecOps:
| Test ID | Category | Example finding |
|---|---|---|
| B101 | assert_used | assert used for security checks (stripped in optimized bytecode) |
| B105-108 | hardcoded_password | Hardcoded credentials in code |
| B201 | flask_debug_true | Flask debug mode enabled |
| B301-303 | pickle | Insecure deserialization with pickle |
| B311 | random | random used instead of secrets for security tokens |
| B320 | xml_bad_element_tree | Vulnerable XML parsing (XXE) |
| B501-506 | ssl/tls | Weak SSL/TLS settings |
| B601-605 | injection | Shell injection risks |
import secrets
import random
# BAD: random is not cryptographically secure
token = random.token_hex(32) # B311: Use of random for security
# GOOD: secrets module is cryptographically secure
token = secrets.token_hex(32)
session_id = secrets.token_urlsafe(32)
Semgrep
Semgrep is a fast, multi-language SAST tool that uses pattern-matching rules. It can be used via CLI or integrated into CI:
pip install semgrep
# Scan with OWASP Top 10 ruleset
semgrep --config p/owasp-top-ten myapp/
# Scan with Python-specific security rules
semgrep --config p/python myapp/
# Scan with multiple rulesets
semgrep --config p/security-audit --config p/secrets myapp/
# Output as SARIF (for GitHub Code Scanning upload)
semgrep --config p/owasp-top-ten --sarif -o semgrep.sarif myapp/
# Python: run Semgrep programmatically and parse results
import subprocess
import json
result = subprocess.run(
['semgrep', '--config', 'p/python', '--json', 'myapp/'],
capture_output=True, text=True
)
findings = json.loads(result.stdout)
for f in findings.get('results', []):
print(f"{f['path']}:{f['start']['line']} — {f['check_id']}: {f['extra']['message']}")
SAST in CI/CD
# GitHub Actions: run Bandit and Semgrep on every push
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Bandit SAST
run: |
pip install bandit
bandit -r src/ -f json -o bandit.json -ll
# Exit 1 on findings stops the pipeline
continue-on-error: false
- name: Semgrep SAST
# The old returntocorp/semgrep-action is retired — run the CLI directly.
run: |
pip install semgrep
semgrep scan --error \
--config p/owasp-top-ten \
--config p/python \
--config p/secrets
- name: Upload SARIF to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: semgrep.sarif
Dynamic Application Security Testing (DAST)
DAST tests a running application from the outside — the way an attacker would — without access to source code.
OWASP ZAP
OWASP ZAP (Zed Attack Proxy) is the leading open-source DAST tool. It intercepts HTTP traffic and actively probes for vulnerabilities.
# Install ZAP (Docker is the easiest method)
docker pull ghcr.io/zaproxy/zaproxy:stable
# Run a baseline (passive) scan against a running app
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t http://localhost:8080 -r zap-report.html
# Run a full active scan (sends attack payloads — use only on test environments)
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py -t http://localhost:8080 -r zap-full-report.html
# Run an API scan against an OpenAPI spec
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py -t http://localhost:8080/openapi.json -f openapi \
-r zap-api-report.html
# Python: control ZAP via its REST API
import time
import requests
ZAP_BASE = 'http://localhost:8080'
TARGET = 'http://testapp:5000'
API_KEY = 'changeme'
# Spider the target
requests.get(f'{ZAP_BASE}/JSON/spider/action/scan/',
params={'apikey': API_KEY, 'url': TARGET})
time.sleep(10)
# Run active scan
resp = requests.get(f'{ZAP_BASE}/JSON/ascan/action/scan/',
params={'apikey': API_KEY, 'url': TARGET})
scan_id = resp.json()['scan']
# Poll until complete
while True:
progress = requests.get(f'{ZAP_BASE}/JSON/ascan/view/status/',
params={'apikey': API_KEY, 'scanId': scan_id}).json()
if progress['status'] == '100':
break
time.sleep(5)
# Retrieve alerts
alerts = requests.get(f'{ZAP_BASE}/JSON/alert/view/alerts/',
params={'apikey': API_KEY}).json()
for alert in alerts['alerts']:
print(f"{alert['risk']}: {alert['alert']} at {alert['url']}")
Nuclei
Nuclei is a fast, template-based vulnerability scanner useful for DAST and CVE checking:
# Install
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Or via prebuilt binary:
curl -s https://api.github.com/repos/projectdiscovery/nuclei/releases/latest \
| grep browser_download_url | grep linux_amd64 | cut -d'"' -f4 \
| xargs curl -Lo nuclei.zip && unzip nuclei.zip && mv nuclei /usr/local/bin/
# Update templates
nuclei -update-templates
# Scan a target
nuclei -u https://testapp.example.com
# Use specific template categories
nuclei -u https://testapp.example.com -t cves/ -t exposures/
# Scan with severity filter
nuclei -u https://testapp.example.com -severity critical,high
# Output in JSON for parsing
nuclei -u https://testapp.example.com -json -o nuclei-results.json
DAST in CI/CD
DAST requires a running application, so it runs later in the pipeline than SAST:
jobs:
dast:
runs-on: ubuntu-latest
needs: [deploy-staging] # requires a running environment
steps:
- name: Run ZAP baseline scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.myapp.example.com'
fail_action: true
rules_file_name: '.zap/rules.tsv' # suppress known false positives
Software Composition Analysis (SCA)
SCA identifies known vulnerabilities (CVEs) in third-party libraries and dependencies.
Trivy
Trivy is the most widely adopted open-source vulnerability scanner. It scans container images, filesystems, Git repositories, Kubernetes clusters, and IaC files.
# Install
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
trivy --version
# Scan a container image
trivy image nginx:latest
trivy image --severity CRITICAL,HIGH nginx:latest
trivy image --exit-code 1 --severity CRITICAL nginx:latest # fail on critical
# Scan a filesystem (Python project)
trivy fs .
trivy fs --severity HIGH,CRITICAL .
# Scan a Git repository
trivy repo https://github.com/myorg/myapp
# Generate SBOM (CycloneDX format)
trivy image --format cyclonedx --output sbom.json nginx:latest
# Scan a running Kubernetes cluster
trivy k8s --report summary cluster
Grype
Grype is an alternative vulnerability scanner from Anchore, focused on container images and filesystems:
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \
| sh -s -- -b /usr/local/bin
grype --version
# Scan an image
grype nginx:latest
# Fail on high+ severity
grype nginx:latest --fail-on high
# Scan a directory
grype dir:.
# Output as JSON
grype nginx:latest -o json > grype-results.json
# Ignore specific CVEs (with justification in .grype.yaml)
# .grype.yaml — project-level Grype configuration
ignore:
- vulnerability: CVE-2021-44228 # Log4j: not reachable in this image
reason: "We use log4j-core 2.17.1 (patched)"
- fix-state: not-fixed # ignore CVEs with no available fix
Safety (Python dependencies)
pip install safety
# `safety check` was retired (unsupported past June 2024) — use `safety scan`,
# which auto-discovers requirements/Poetry/Pipenv files in the project directory.
# Scan the project (current directory)
safety scan
# JSON output
safety scan --output json
# Use in CI — `scan` exits non-zero when vulnerabilities are found.
# Safety 3.x requires auth: run `safety auth` once, or set SAFETY_API_KEY in CI.
safety scan
# Parse safety output in Python
import subprocess
import json
result = subprocess.run(
['safety', 'check', '--json', '-r', 'requirements.txt'],
capture_output=True, text=True
)
data = json.loads(result.stdout)
for vuln in data.get('vulnerabilities', []):
print(f"{vuln['package_name']} {vuln['analyzed_version']}: "
f"{vuln['vulnerability_id']} — {vuln['advisory']}")
Secrets Detection
Hardcoded secrets (API keys, passwords, tokens) in source code are one of the most common and most damaging vulnerabilities. Once committed to a repository — even if later deleted — secrets are permanently in the git history unless the entire history is rewritten.
gitleaks
gitleaks scans git repositories for secrets using regular expression rules and entropy analysis:
# Install (pinned version — the tag and the filename must match)
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \
| tar -xz -C /usr/local/bin gitleaks
# Scan the current repo (full history)
gitleaks git . --verbose
# Scan a specific commit range (see caution below)
gitleaks git . --log-opts="HEAD~10..HEAD"
# Scan a directory of files (no git context, working tree only)
gitleaks dir .
# Scan piped input
git diff | gitleaks stdin
# Output report as JSON
gitleaks git . --report-format json --report-path gitleaks-report.json
# Use as a pre-commit hook
# Install: pip install pre-commit
# .pre-commit-config.yaml:
# - repo: https://github.com/gitleaks/gitleaks
# rev: v8.30.1
# hooks:
# - id: gitleaks
The git vs. dir distinction is the one that matters: dir sees only the working
tree, so a credential that was committed and later deleted produces zero findings
from dir and a finding from git. Scan history.
⚠️ These subcommands were introduced in gitleaks 8.19, replacing
detect --source .(nowgit),detect --no-git(nowdir), anddetect --pipe(nowstdin). The old forms still execute but are hidden from--helpand may eventually be removed — most documentation and tutorials online still use them.
⚠️ A
--log-optsrange wider than the available history is a silent pass. On a repo with only two commits,--log-opts="HEAD~10..HEAD"reports0 commits scannedandno leaks found— exit code 0, nothing scanned. The same trap applies to shallow CI clones, which is why secret-scanning jobs needfetch-depth: 0. Always check the commit count in the output, not just the leak count.
truffleHog
truffleHog uses entropy analysis and regular expressions to find secrets, and can scan git history, S3 buckets, and Slack:
# Install
pip install trufflehog3
# Or download the binary:
curl -sSfL https://github.com/trufflesecurity/trufflehog/releases/latest/download/trufflehog_3.88.1_linux_amd64.tar.gz \
| tar -xz -C /usr/local/bin
# Scan a git repo
trufflehog git https://github.com/myorg/myapp --only-verified
# Scan a local repo
trufflehog git file://. --only-verified
# Scan a single file
trufflehog filesystem ./config.yaml
# JSON output
trufflehog git file://. --json
Pre-commit hooks for secrets prevention
The best time to catch a secret is before it is committed:
# Install pre-commit
pip install pre-commit
# .pre-commit-config.yaml
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key
- id: detect-aws-credentials
EOF
# Install the hooks
pre-commit install
# Run manually
pre-commit run --all-files
SBOM Generation
A Software Bill of Materials (SBOM) is a machine-readable inventory of all software components in an application — including transitive dependencies. SBOMs are required by US Executive Order 14028 for software sold to the federal government and are an emerging best practice for all software.
SBOM formats
| Format | Specification | Typical use |
|---|---|---|
| CycloneDX | OWASP | Most tooling support; recommended for vulnerability correlation |
| SPDX | Linux Foundation | Legal/license compliance focus; ISO standard |
Syft — SBOM generation
Syft (from Anchore) generates SBOMs from container images, directories, and archives:
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
| sh -s -- -b /usr/local/bin
# Generate SBOM for a container image (CycloneDX)
syft nginx:latest -o cyclonedx-json=sbom.json
# Generate SBOM for a Python project directory
syft dir:. -o spdx-json=sbom-spdx.json
# Generate SBOM in multiple formats
syft nginx:latest -o cyclonedx-json -o spdx-json=sbom.spdx.json
# Pipe SBOM into Grype for vulnerability matching
syft nginx:latest -o json | grype
Trivy SBOM generation
Trivy can generate SBOMs as part of its image scan:
# Generate CycloneDX SBOM
trivy image --format cyclonedx --output sbom.cdx.json nginx:latest
# Generate SPDX SBOM
trivy image --format spdx-json --output sbom.spdx.json nginx:latest
Cosign — artifact signing
Cosign (part of the Sigstore project) signs container images and other artifacts so consumers can verify their authenticity and integrity:
# Install
curl -O https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
mv cosign-linux-amd64 /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
# Sign an image (keyless — uses OIDC identity, stored in Rekor transparency log)
cosign sign ghcr.io/myorg/myapp:v1.0.0
# Sign with a key pair
cosign generate-key-pair # creates cosign.key and cosign.pub
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.0.0
# Verify a signature
cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.0.0
# Attach an SBOM to the image
cosign attach sbom --sbom sbom.cdx.json ghcr.io/myorg/myapp:v1.0.0
# Verify the SBOM attachment
cosign verify-attestation --key cosign.pub --type cyclonedx \
ghcr.io/myorg/myapp:v1.0.0
Worked example — signed image build and verify pipeline:
# GitHub Actions: build, scan, sign, verify
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
id-token: write # required for keyless signing
packages: write
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t ghcr.io/$:$ .
- name: Scan with Trivy
# Pin to a full commit SHA, not a moving ref: trivy-action tags were
# force-pushed in a March 2026 supply-chain compromise.
uses: aquasecurity/trivy-action@<commit-sha> # SHA for a known-good release
with:
image-ref: ghcr.io/$:$
severity: CRITICAL
exit-code: 1
- name: Push image
run: docker push ghcr.io/$:$
- name: Sign image with cosign (keyless)
uses: sigstore/cosign-installer@v3
- run: |
cosign sign --yes ghcr.io/$:$
Key takeaways
- The OWASP Top 10 is the shared map of web risk — Broken Access Control, Cryptographic Failures, and Injection lead it. Know what each category means and how it’s mitigated.
- Parameterized queries are the only reliable SQLi defense, and context-aware output encoding (plus CSP) is the XSS defense. Validate input with allowlists at the trust boundary, never denylists.
- The testing types are complementary: SAST reads code (Bandit, Semgrep), DAST attacks the running app (ZAP, Nuclei), SCA finds vulnerable dependencies (Trivy, Grype, Safety), and secrets detection (gitleaks, truffleHog) catches credentials before they hit history.
- Shift left, then gate: run SAST/SCA/secrets on every push and DAST against staging, failing the pipeline on high-severity findings — a finding that blocks merge costs far less than one in production.
- Provenance matters: generate an SBOM (CycloneDX/SPDX with Syft or Trivy) and sign artifacts with Cosign/Sigstore so consumers can verify what they run — increasingly a compliance requirement (EO 14028).
- Use
secrets, notrandom, for anything security-sensitive, and never useassertfor security checks (it’s stripped under-O).
References
- OWASP Top 10 (2021). https://owasp.org/www-project-top-ten/
- OWASP Application Security Verification Standard (ASVS). https://owasp.org/www-project-application-security-verification-standard/
- Bandit — Python SAST. https://bandit.readthedocs.io/
- Semgrep documentation. https://semgrep.dev/docs/
- OWASP ZAP documentation. https://www.zaproxy.org/docs/
- Trivy — SCA and SBOM. https://trivy.dev/latest/
- Syft — SBOM generation. https://github.com/anchore/syft
- Cosign / Sigstore — artifact signing. https://docs.sigstore.dev/
- CISA — SBOM guidance. https://www.cisa.gov/sbom
Related course pages: Vulnerability Management · Software Supply-Chain Security · DevSecOps Fundamentals
🛠️ Maintenance note: the OWASP Top 10 2025 edition is in release-candidate stage and will reshuffle these categories — update the table when it’s final. Tool pins drift fast: re-verify the gitleaks/truffleHog/Syft version strings in the install commands, and replace the
<commit-sha>placeholders ontrivy-actionwith a current known-good commit SHA (its tags were force-pushed in a March 2026 supply-chain compromise). Re-verify each term.