name: sast-patterns description: Static Application Security Testing patterns, OWASP Top 10 checklist, language-specific vulnerability patterns, Semgrep rule writing guide, and CI/CD integration. Use when scanning code for security vulnerabilities or writing custom SAST rules.
SAST Patterns Skill
Comprehensive vulnerability pattern library for static application security testing. Covers OWASP Top 10, language-specific patterns, Semgrep custom rules, and CI/CD pipeline integration.
When to Activate
- Running security scans on code
- Writing custom Semgrep rules
- Setting up CI/CD security gates
- Reviewing code for security vulnerabilities
- After sast-on-edit hook triggers
- Before production deployment
OWASP Top 10 (2021) Checklist
A01: Broken Access Control
What to Look For:
- Missing authorization checks on endpoints
- Direct object reference without ownership validation
- CORS misconfiguration allowing wildcard origins
- Missing function-level access control
- Metadata manipulation (JWT, cookies, hidden fields)
Detection Patterns:
// VULNERABLE: No authorization check
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id)
res.json(user) // Anyone can access any user
})
// SECURE: Authorization verified
app.get('/api/users/:id', authenticate, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' })
}
const user = await db.users.findById(req.params.id)
res.json(user)
})
# VULNERABLE: No permission check
@app.route('/admin/delete/<user_id>', methods=['DELETE'])
def delete_user(user_id):
db.session.delete(User.query.get(user_id))
db.session.commit()
# SECURE: Permission verified
@app.route('/admin/delete/<user_id>', methods=['DELETE'])
@login_required
@admin_required
def delete_user(user_id):
db.session.delete(User.query.get(user_id))
db.session.commit()
Semgrep Rules:
rules:
- id: missing-auth-check
patterns:
- pattern: |
app.$METHOD($PATH, async (req, res) => {
...
$DB.$QUERY(...)
...
})
- pattern-not: |
app.$METHOD($PATH, authenticate, ...)
message: "Endpoint missing authentication middleware"
severity: ERROR
A02: Cryptographic Failures
What to Look For:
- Hardcoded secrets in source code
- Weak hashing (MD5, SHA1) for passwords
- Missing encryption for sensitive data at rest
- HTTP instead of HTTPS
- Weak TLS configuration
Detection Patterns:
// VULNERABLE: Weak password hashing
const hash = crypto.createHash('md5').update(password).digest('hex')
// SECURE: Strong password hashing
const hash = await bcrypt.hash(password, 12)
# VULNERABLE: Hardcoded secret
SECRET_KEY = "my-super-secret-key-123"
# SECURE: Environment variable
SECRET_KEY = os.environ.get('SECRET_KEY')
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable required")
Regex Patterns for Detection:
# API Keys
(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}['"]
# AWS Keys
(?:AKIA|ASIA)[A-Z0-9]{16}
# Generic Secrets
(?:password|passwd|pwd|secret|token)\s*[:=]\s*['"][^'"]{8,}['"]
# Private Keys
-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----
# JWT Tokens
eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+
A03: Injection
What to Look For:
- SQL injection (string concatenation in queries)
- Command injection (unsanitized shell execution)
- NoSQL injection (MongoDB query operators in user input)
- LDAP injection
- Expression Language injection
Detection Patterns:
// SQL Injection
const query = `SELECT * FROM users WHERE id = ${userId}` // VULNERABLE
const query = 'SELECT * FROM users WHERE id = $1' // SECURE
// Command Injection
exec(`ping ${hostname}`) // VULNERABLE
execFile('ping', [hostname]) // SECURE
// NoSQL Injection (MongoDB)
db.users.find({ email: req.body.email }) // VULNERABLE if email = {"$gt": ""}
db.users.find({ email: String(req.body.email) }) // SECURE: type coercion
# SQL Injection
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # VULNERABLE
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) # SECURE
# Command Injection
os.system(f"convert {filename} output.png") # VULNERABLE
subprocess.run(['convert', filename, 'output.png'], check=True) # SECURE
// SQL Injection
db.Query("SELECT * FROM users WHERE id = " + userID) // VULNERABLE
db.Query("SELECT * FROM users WHERE id = $1", userID) // SECURE
// Command Injection
exec.Command("sh", "-c", userInput) // VULNERABLE
exec.Command("ping", "-c", "1", hostname) // SECURE
// SQL Injection
stmt.executeQuery("SELECT * FROM users WHERE id = " + id); // VULNERABLE
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setString(1, id); // SECURE
A04: Insecure Design
What to Look For:
- Missing rate limiting on critical operations
- No account lockout after failed attempts
- Missing CAPTCHA on public forms
- Business logic flaws
- Missing transaction atomicity
Detection Patterns:
// VULNERABLE: No rate limiting on login
app.post('/login', async (req, res) => {
const user = await authenticate(req.body)
res.json({ token: generateToken(user) })
})
// SECURE: Rate limited login
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts'
})
app.post('/login', loginLimiter, async (req, res) => {
const user = await authenticate(req.body)
res.json({ token: generateToken(user) })
})
# VULNERABLE: Race condition in balance check
balance = get_balance(user_id)
if balance >= amount:
withdraw(user_id, amount) # Another request could drain first
# SECURE: Atomic transaction
with db.transaction():
balance = db.query("SELECT balance FROM accounts WHERE id = %s FOR UPDATE", user_id)
if balance >= amount:
db.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", amount, user_id)
A05: Security Misconfiguration
What to Look For:
- Debug mode in production
- Default credentials
- Unnecessary features enabled
- Missing security headers
- Verbose error messages
- Directory listing enabled
Detection Patterns:
// VULNERABLE: Debug mode
app.set('env', 'development') // In production config
// VULNERABLE: Missing security headers
// No helmet or manual headers
// SECURE: Security headers
import helmet from 'helmet'
app.use(helmet())
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
}
}))
# VULNERABLE: Debug in production
DEBUG = True # In production settings
# VULNERABLE: Default secret key
SECRET_KEY = 'django-insecure-change-me'
# SECURE
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
Security Headers Checklist:
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
A06: Vulnerable and Outdated Components
Detection Commands:
# Node.js
npm audit
npm audit --json
npm outdated
# Python
pip-audit
safety check
pip list --outdated
# Go
go list -m -u all
govulncheck ./...
# Java
mvn dependency-check:check
gradle dependencyCheckAnalyze
# Ruby
bundle audit check
A07: Identification and Authentication Failures
What to Look For:
- Weak password policies
- Missing MFA
- Session fixation
- Credential stuffing vulnerability
- Plaintext password storage/comparison
Detection Patterns:
// VULNERABLE: Plaintext password comparison
if (password === user.password) { /* login */ }
// SECURE: Hashed comparison
const isValid = await bcrypt.compare(password, user.passwordHash)
// VULNERABLE: Weak session management
app.use(session({ secret: 'secret' }))
// SECURE: Strong session
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000
}
}))
A08: Software and Data Integrity Failures
What to Look For:
- Missing Subresource Integrity (SRI) on CDN scripts
- Insecure deserialization
- Missing code signing
- Auto-update without integrity verification
- CI/CD pipeline without integrity checks
Detection Patterns:
<!-- VULNERABLE: No SRI -->
<script src="https://cdn.example.com/lib.js"></script>
<!-- SECURE: With SRI -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>
# VULNERABLE: Insecure deserialization
import pickle
data = pickle.loads(user_input)
# SECURE: Use JSON
import json
data = json.loads(user_input)
A09: Security Logging and Monitoring Failures
What to Look For:
- Missing audit logs for auth events
- Sensitive data in logs
- No log integrity protection
- Missing alerting for suspicious activity
Detection Patterns:
// VULNERABLE: No logging
app.post('/login', async (req, res) => {
const user = await authenticate(req.body)
if (!user) return res.status(401).json({ error: 'Invalid' })
res.json({ token: generateToken(user) })
})
// SECURE: Audit logging
app.post('/login', async (req, res) => {
const user = await authenticate(req.body)
if (!user) {
logger.warn('Failed login attempt', {
email: req.body.email,
ip: req.ip,
timestamp: new Date().toISOString()
})
return res.status(401).json({ error: 'Invalid credentials' })
}
logger.info('Successful login', { userId: user.id, ip: req.ip })
res.json({ token: generateToken(user) })
})
// VULNERABLE: Sensitive data in logs
console.log('Login:', { email, password, token })
// SECURE: Redacted logs
console.log('Login:', { email, passwordProvided: !!password })
A10: Server-Side Request Forgery (SSRF)
What to Look For:
- User-controlled URLs in server-side requests
- Missing URL validation/whitelist
- Internal service access via SSRF
- Cloud metadata endpoint access
Detection Patterns:
// VULNERABLE: SSRF
const response = await fetch(req.query.url)
// SECURE: URL whitelist
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com']
const url = new URL(req.query.url)
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed')
}
// Also block internal IPs
const ip = await dns.resolve(url.hostname)
if (isPrivateIP(ip)) {
throw new Error('Internal addresses not allowed')
}
const response = await fetch(url.toString())
# VULNERABLE: SSRF
response = requests.get(user_url)
# SECURE: URL validation
from urllib.parse import urlparse
parsed = urlparse(user_url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("Domain not allowed")
if is_private_ip(socket.gethostbyname(parsed.hostname)):
raise ValueError("Internal addresses blocked")
response = requests.get(user_url, allow_redirects=False)
Semgrep Custom Rule Writing Guide
Basic Rule Structure
rules:
- id: rule-unique-id
pattern: |
eval($USER_INPUT)
message: "eval() with dynamic input detected - potential RCE"
languages: [javascript, typescript]
severity: ERROR
metadata:
cwe:
- CWE-94
owasp:
- A03:2021
category: security
confidence: HIGH
Pattern Operators
# pattern: match exactly
- pattern: eval($X)
# pattern-not: exclude matches
- pattern-not: eval("static-string")
# patterns: AND (all must match)
- patterns:
- pattern: $DB.query($SQL)
- pattern-not: $DB.query($SQL, $PARAMS)
# pattern-either: OR (any can match)
- pattern-either:
- pattern: eval($X)
- pattern: new Function($X)
# pattern-inside: match within a context
- pattern-inside: |
app.$METHOD($PATH, (req, res) => {
...
})
# pattern-not-inside: exclude context
- pattern-not-inside: |
app.$METHOD($PATH, authenticate, ...)
# pattern-regex: regex in code
- pattern-regex: "password\s*=\s*['\"][^'\"]{3,}['\"]"
Metavariable Constraints
rules:
- id: weak-hash-for-passwords
patterns:
- pattern: crypto.createHash($ALG).update($INPUT)
- metavariable-regex:
metavariable: $ALG
regex: "('md5'|'sha1')"
- metavariable-regex:
metavariable: $INPUT
regex: ".*password.*"
message: "Weak hash algorithm used for password: $ALG"
languages: [javascript, typescript]
severity: ERROR
Taint Analysis (Pro)
rules:
- id: sql-injection-taint
mode: taint
pattern-sources:
- pattern: req.body.$PARAM
- pattern: req.query.$PARAM
- pattern: req.params.$PARAM
pattern-sinks:
- pattern: $DB.query($SQL)
pattern-sanitizers:
- pattern: $DB.escape($X)
- pattern: sanitize($X)
message: "User input flows to SQL query without sanitization"
languages: [javascript]
severity: ERROR
Project .semgrep.yml Example
rules:
- id: no-hardcoded-secrets
pattern-regex: |
(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][A-Za-z0-9+/=_\-]{16,}['"]
paths:
exclude:
- "*.test.*"
- "*.spec.*"
- "__tests__/*"
- "*.example"
message: "Potential hardcoded secret detected"
languages: [generic]
severity: ERROR
- id: no-console-log-sensitive
patterns:
- pattern: console.log(..., $DATA, ...)
- metavariable-regex:
metavariable: $DATA
regex: ".*(?:password|secret|token|key|credential).*"
message: "Sensitive data in console.log"
languages: [javascript, typescript]
severity: WARNING
- id: require-input-validation
patterns:
- pattern: |
app.post($PATH, async (req, res) => {
...
$DB.$METHOD(req.body)
...
})
- pattern-not: |
app.post($PATH, async (req, res) => {
...
$SCHEMA.parse(...)
...
})
message: "POST endpoint without input validation"
languages: [javascript, typescript]
severity: WARNING
CI/CD Integration
GitHub Actions
name: SAST Security Scan
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semgrep Scan
uses: semgrep/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/secrets
p/typescript
generateSarif: "1"
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
if: always()
- name: Dependency Audit
run: npm audit --audit-level=high
- name: Check for Secrets
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/semgrep/semgrep
rev: 'v1.60.0'
hooks:
- id: semgrep
args: ['--config', 'auto', '--error', '--severity', 'ERROR']
GitLab CI
sast:
stage: test
image: semgrep/semgrep
script:
- semgrep --config auto --config "p/secrets" --json --output gl-sast-report.json .
artifacts:
reports:
sast: gl-sast-report.json
rules:
- if: $CI_MERGE_REQUEST_ID
CLI Commands Quick Reference
# Full scan with auto rules
semgrep --config auto .
# OWASP + Secrets
semgrep --config "p/owasp-top-ten" --config "p/secrets" .
# Only errors (for CI gates)
semgrep --config auto --error --severity ERROR .
# JSON output for processing
semgrep --config auto --json --output report.json .
# Scan specific files
semgrep --config auto src/api/ src/auth/
# Diff-aware (only changed code)
semgrep --config auto --baseline-commit origin/main
# Custom rules
semgrep --config .semgrep.yml .
# Exclude test files
semgrep --config auto --exclude="*test*" --exclude="*spec*" .
# With metrics disabled (privacy)
semgrep --config auto --metrics=off .
Severity Classification Guide
| Severity | Criteria | Action | SLA |
|---|---|---|---|
| CRITICAL | Exploitable RCE, SQLi, auth bypass, data breach | Fix IMMEDIATELY, block deploy | < 1 hour |
| HIGH | XSS, SSRF, IDOR, missing auth, weak crypto | Fix before production | < 24 hours |
| MEDIUM | Missing validation, verbose errors, weak headers | Fix in current sprint | < 1 week |
| LOW | Debug mode, outdated lib (no known CVE), info leak | Fix in backlog | < 1 month |
Quick Decision Tree
Is user input involved?
YES -> Does it reach a dangerous sink (DB, exec, DOM)?
YES -> Is it sanitized/validated?
NO -> CRITICAL (injection)
YES -> Check sanitizer adequacy -> MEDIUM if weak
NO -> MEDIUM (missing validation)
NO -> Is it a configuration issue?
YES -> Affects security posture?
YES -> HIGH (misconfiguration)
NO -> LOW (best practice)
NO -> Is it a dependency issue?
YES -> Known CVE?
YES -> Match CVE severity
NO -> LOW (outdated)
NO -> Informational
Resources
- OWASP Top 10 (2021)
- Semgrep Registry
- Semgrep Rule Syntax
- CWE Database
- NIST NVD
- Snyk Vulnerability DB
Remember: SAST catches patterns, not intent. Always verify findings with manual review. A finding is only a vulnerability if it can be exploited in the application's specific context.