Module 1: Introduction to Source Code Review
Read the application like an attacker: identify what enters, what is trusted, what is executed, and what is returned.
What is Source Code Review?
Source code review is the manual and tool-assisted examination of an application's code to find security weaknesses before or alongside runtime testing. It reveals the decisions hidden behind an endpoint: how identity is established, where authorization is enforced, how input reaches dangerous operations, and whether business rules can be bypassed.
Dynamic testing shows what happened for one request. Source review explains every path that could make it happen.
Core Definitions
Entry Point — Code that receives an event or untrusted data: HTTP routes, controllers, RPC handlers, message consumers, scheduled jobs, webhooks, file imports, CLI arguments, or mobile deep links.
Source — The origin of data an attacker may influence: parameters, headers, cookies, database records, queues, files, environment variables, or third-party responses.
Sink — A security-sensitive operation: SQL execution, command execution, template rendering, file access, outbound requests, deserialization, redirects, cryptographic operations, or authorization decisions.
Sanitizer — A context-specific control that makes data safe for one sink, such as parameterized SQL, HTML output encoding, or a strict allowlist. A control for one context is not automatically safe for another.
Trust Boundary — A point where data or authority crosses between different trust levels: internet to API, user to administrator, tenant A to tenant B, application to database, or CI runner to production.
Data Flow — The path a value follows from a source through transformations to a sink.
Call Graph — The chain of functions and methods that can invoke one another.
Reachability — Whether attacker-controlled data can actually reach vulnerable code under realistic conditions.
Review Mindset
For every sensitive action, answer five questions:
- Who can reach it?
- What data can they control?
- What security decision is made?
- Where is that decision enforced?
- Can another path reach the same sink without the control?
Do not stop at a suspicious line. Prove the complete path:
Attacker-controlled source
↓
Validation / transformation
���
Authentication and authorization
↓
Security-sensitive sink
↓
Observable impact
Manual Review vs Automated Scanning
| Manual review is strongest at | Automated scanning is strongest at |
|---|---|
| Business logic and workflow abuse | Repeated dangerous patterns |
| Missing authorization | Known vulnerable dependencies |
| Tenant isolation | Large-scale source-to-sink discovery |
| Trust boundary mistakes | Secret and credential patterns |
| Framework-specific misuse | Consistent baseline checks |
| Chained vulnerabilities | Fast regression scanning |
Use tools to create leads. Treat a finding as confirmed only after checking reachability, control effectiveness, and impact.
The Basic Review Loop
Inventory → Map entry points → Identify sensitive sinks → Trace data → Verify controls → Prove impact → Report root cause
Module 2: Scope, Setup & Repository Reconnaissance
Freeze the revision, understand what is in scope, and remove noise before reading individual functions.
1. Minimum Inputs
Request these before starting:
- Repository or source archive and the exact branch, tag, or commit in scope.
- Build and run instructions.
- Architecture and data-flow diagrams, if available.
- API documentation, route inventory, and sample requests.
- Test accounts for each role and tenant.
- Environment and deployment model: cloud, containers, serverless, mobile, or on-premises.
- Known exclusions: generated code, third-party code, deprecated services, or test fixtures.
- Previous findings and accepted risks.
If an item is missing, record the limitation. Never silently assume full coverage.
2. Freeze the Reviewed Revision
git rev-parse --verify HEAD
git branch --show-current
git status --short
git log -1 --format='%H %cI %s'
Record the full commit SHA in the report. If code changes during the assessment, review the delta separately.
The examples below assume Bash and a GNU/Linux userland. Replace uppercase placeholders with real values; do not type angle brackets because shells interpret < and > as redirection operators.
git diff --stat REVIEWED_COMMIT..NEW_COMMIT
git diff REVIEWED_COMMIT..NEW_COMMIT -- path/to/security-sensitive-code
3. Inventory the Repository
# List tracked files
git ls-files
# Summarize common source types
git ls-files | awk -F. 'NF>1 {print tolower($NF)}' | sort | uniq -c | sort -nr | head -40
# Find large files that may be generated, vendored, or binary
git ls-files -z | xargs -0 -r du -h -- | sort -h | tail -30
# Quick directory view, excluding common dependency folders
find . -maxdepth 3 -type d \
-not -path '*/.git*' \
-not -path '*/node_modules*' \
-not -path '*/vendor*' \
-not -path '*/dist*' | sort
4. Identify the Technology Stack
| File | Likely stack or purpose |
|---|---|
pom.xml, build.gradle |
Java / Kotlin |
*.csproj, *.sln |
.NET |
package.json, lockfiles |
JavaScript / TypeScript |
requirements.txt, pyproject.toml |
Python |
composer.json |
PHP |
go.mod |
Go |
Gemfile |
Ruby |
Cargo.toml |
Rust |
Dockerfile, compose.yaml |
Containers |
*.tf, serverless.yml |
Infrastructure / serverless |
.github/workflows/*.yml |
GitHub Actions |
find . -maxdepth 4 -type f \( \
-name 'pom.xml' -o -name 'build.gradle*' -o -name '*.csproj' \
-o -name 'package.json' -o -name 'requirements*.txt' \
-o -name 'pyproject.toml' -o -name 'composer.json' \
-o -name 'go.mod' -o -name 'Gemfile' -o -name 'Cargo.toml' \
-o -name 'Dockerfile*' -o -name '*.tf' \
\) -print
5. Separate First-Party Code from Noise
Usually exclude or review separately:
- Generated clients, migrations, and compiled assets.
- Dependency folders such as
node_modules,vendor, and package caches. - Minified JavaScript and source maps.
- Test data that cannot reach production.
- Forked libraries maintained outside the application team.
Do not exclude code merely because it is old. Dead-looking routes, migration utilities, debug handlers, and administrative scripts often remain deployable.
6. Start with Recent Security-Sensitive Changes
# Recent changes to authentication, authorization, upload, payment, and configuration code
git log --oneline --all -- \
'*auth*' '*login*' '*permission*' '*role*' '*upload*' '*payment*' '*config*'
# Search commit messages for security-relevant changes
git log --all --oneline --grep='auth\|security\|permission\|bypass\|hotfix\|secret' -i
# Identify frequently changed files
git log --all --name-only --pretty=format: | \
sed '/^$/d' | sort | uniq -c | sort -nr | head -40
Recent changes are a starting point, not the full scope.
7. Review Order
Use this order when time is limited:
- Authentication, password reset, MFA, and token verification.
- Authorization and tenant filtering.
- Administrative, payment, export, webhook, upload, and integration features.
- SQL, OS command, template, file, deserialization, and outbound-request sinks.
- Secrets, configuration, dependencies, and CI/CD.
- Remaining routes and background workers.
8. Review Notes Template
Review target:
Repository:
Branch / tag:
Commit SHA:
Build profile:
Languages / frameworks:
Entry points:
Data stores:
External services:
User roles:
Tenant model:
Excluded paths:
Limitations:
Completion Criteria
- Exact revision recorded.
- All first-party components inventoried.
- Entry points and sensitive sinks mapped.
- Roles and tenant boundaries understood.
- Tool findings manually triaged.
- Confirmed issues include a complete source-to-impact path.
- Unreviewed areas and limitations documented.
Module 3: Architecture & Attack Surface Mapping
Build a small map of the application before following individual variables.
1. Understand the Layers
A common request path looks like this:
Request
→ Router
→ Middleware / Filter / Interceptor
→ Controller or Protocol Handler
→ Service / Use Case
→ Repository / External Client
→ Database, Queue, File, or Third-Party API
→ Response Mapper
These names are conventions, not security boundaries. A project may combine several layers or use different names.
| Layer | Normal responsibility | Security questions |
|---|---|---|
| Router | Match method, path, host, content type, or protocol action | Which handler wins? Which middleware actually applies? Are old or wildcard routes exposed? |
| Middleware / filter | Authentication, request limits, CSRF, tenant context, logging | Is the order correct? Can a route skip it? Does failure stop processing? |
| Controller | Parse transport input and call a use case | Which fields are attacker-controlled? Is identity passed forward? Are framework-bound models over-permissive? |
| RPC handler / resolver | Handle an RPC method, GraphQL field, or message action | Is each method/action authorized? Is metadata trusted? Are stream messages rechecked? |
| Service / use case | Enforce workflow and business rules | Is the server the source of truth? Are state transitions and separation of duties enforced? |
| Repository | Query or modify persistent data | Is tenant/owner scope part of the query? Is query structure parameterized? |
| External client | Call another service or platform | Is the destination fixed? Are responses trusted safely? Are credentials scoped? |
| Response mapper | Select and serialize output fields | Can secret or privileged properties leave the service? |
The key review rule is simple: follow the data and identity across every layer. A secure-looking route is not enough if the service or repository loses the caller's tenant and ownership context.
2. Routes in Detail
A route connects an incoming HTTP request to code.
router.get(
'/api/orders/:orderId',
requireLogin,
validateOrderId,
orderController.getOrder
);
This tells the reviewer:
GETis the accepted method.orderIdis attacker-controlled path input.requireLoginshould establish identity.validateOrderIdshould validate syntax, not ownership.getOrderis the controller entry point.
What to inspect:
- Where is this router mounted?
/api,/internal, or more than once? - Does
requireLoginreturn immediately after failure, or can execution continue? - Is a broader route registered first and shadowing this route?
- Is the same controller reachable through another route without middleware?
- Are
HEAD,OPTIONS, method override, alternate content types, and trailing-slash behavior relevant? - Are route parameters decoded or normalized more than once?
- Does a reverse proxy rewrite the path or supply trusted headers?
Route-order example:
router.use('/admin', publicStaticHandler);
router.use('/admin', requireAdmin, adminRouter);
Do not assume the second line protects the first. Review which paths publicStaticHandler can serve and whether it terminates the request.
3. Controllers in Detail
A controller translates HTTP input into an application call and translates the result into an HTTP response.
async function getOrder(req, res) {
const order = await orderService.getById(req.params.orderId);
return res.json(order);
}
Immediate review observations:
req.params.orderIdis a source.req.useris never passed to the service.- The controller returns the full object.
- Authentication may exist, but object-level authorization is not visible.
Continue into the service and repository before confirming a finding:
// service/order-service.js
async function getById(orderId) {
return orderRepository.findById(orderId);
}
// repository/order-repository.js
async function findById(orderId) {
return db.order.findUnique({ where: { id: orderId } });
}
The gap is now clear: no layer compares the order's owner or tenant with authenticated identity.
Safer data flow:
// route/controller
const order = await orderService.getForPrincipal({
orderId: req.params.orderId,
userId: req.user.id,
tenantId: req.user.tenantId
});
// repository: authorization scope is part of the query
return db.order.findFirst({
where: {
id: orderId,
tenantId,
ownerId: userId
},
select: {
id: true,
status: true,
total: true
}
});
How to identify the issue
- Mark
orderIdas attacker-controlled. - Locate where authentication creates
req.user. - Follow the controller call into the service.
- Confirm that identity and tenant context disappear.
- Inspect the repository query and find that it filters only by object ID.
- Inspect the response and identify exposed fields.
- Search for every caller of
getByIdandfindByIdto find equivalent paths.
Reference tests
| Test | Input / principal | Secure result |
|---|---|---|
| Own object | User A requests Order A | 200 with allowed fields |
| Same-tenant peer | User A requests User B's order | 403 or non-enumerating 404 |
| Cross-tenant object | Tenant A user requests Tenant B order | 403 or non-enumerating 404 |
| Anonymous | No session requests an order | 401 |
| Malformed identifier | Invalid ID shape | 400, without stack trace |
| Privileged support role | Authorized support user requests object | Allowed only through documented, audited policy |
4. Middleware, Filters and Interceptors
Middleware runs before, after, or around a handler. Frameworks may call the same concept a filter, guard, policy, interceptor, dependency, or hook.
async function requireLogin(req, res, next) {
const user = await sessions.resolve(req.cookies.session);
if (!user) {
res.status(401).json({ error: 'unauthorized' });
return;
}
req.user = user;
return next();
}
Review gaps:
- Missing
returnor equivalent after denial. - Exceptions caught and converted into
next(). - Authentication middleware registered after the route.
- Route groups mounted outside the protected prefix.
- Optional authentication reused for a required-auth route.
- Tenant context accepted from a header without binding it to the principal.
- Security logic based on a spoofable proxy header.
- Internal recursion or subrequest headers accepted directly from the internet.
Search both the middleware definition and every place it is registered.
5. RPC and gRPC Handlers in Detail
RPC exposes named methods rather than HTTP resources. gRPC usually defines those methods in a Protocol Buffers service.
service DocumentService {
rpc GetDocument(GetDocumentRequest) returns (Document);
rpc ExportDocuments(ExportRequest) returns (stream DocumentChunk);
}
message GetDocumentRequest {
string document_id = 1;
}
The .proto file is the route inventory. Every rpc method is an entry point; request fields are sources.
Example handler:
async function getDocument(call, callback) {
const principal = call.context.principal; // set by an authentication interceptor
const document = await documents.findById(call.request.document_id);
callback(null, document);
}
The interceptor may prove who called, but this handler never checks whether the principal can access the document.
Safer pattern:
async function getDocument(call, callback) {
const { principal } = call.context;
const document = await documents.findForTenant(
call.request.document_id,
principal.tenantId
);
if (!document || !policy.canRead(principal, document)) {
return callback({ code: grpc.status.NOT_FOUND });
}
return callback(null, mapPublicDocument(document));
}
What to inspect in RPC code:
- Server interceptors and the exact services/methods they cover.
- Identity in metadata: who creates it and whether clients can spoof it.
- Method-level, object-level, and field-level authorization.
- Protobuf defaults: omitted booleans and numeric fields may become valid-looking zero values.
oneof, wrapper types, unknown fields, maximum message size, and recursion depth.- Deadline and cancellation propagation to databases and downstream calls.
- Error mapping: internal exceptions must not expose stack traces or secrets.
- Unary, client-streaming, server-streaming, and bidirectional-streaming behavior.
- Reflection and health services exposed outside intended networks.
Streaming-specific gap
function updateDocuments(stream) {
const principal = stream.context.principal;
stream.on('data', async message => {
await documents.update(message.document_id, message.patch);
});
}
Authenticating once when the stream opens is not enough. Every message can reference a different object and needs object/property authorization. Also consider whether role or account status can change during a long-lived stream.
RPC reference tests
| Test | Secure result |
|---|---|
| Call method without credentials | UNAUTHENTICATED |
| Call privileged method as normal user | PERMISSION_DENIED |
| Request another tenant's object | NOT_FOUND or PERMISSION_DENIED by policy |
| Omit security-sensitive protobuf field | Safe default or validation failure |
| Send unexpected enum / unknown field | Rejected or safely ignored without changing authorization |
| Change object ID between stream messages | Every message is independently authorized |
| Cancel the RPC during a downstream write | Cancellation follows the documented transaction or idempotency policy; no unintended partial state remains |
| Exceed message or stream limits | Bounded failure without resource exhaustion |
6. GraphQL Resolvers
GraphQL often has one HTTP endpoint but many real entry points: queries, mutations, subscriptions, and field resolvers.
const resolvers = {
Query: {
invoice: (_, { id }, ctx) => invoiceRepo.findById(id)
},
Invoice: {
internalNotes: invoice => invoice.internalNotes
}
};
Potential gaps:
- The top-level query lacks object authorization.
- The nested
internalNotesfield lacks property-level authorization. - Aliases or batching multiply sensitive operations and bypass route-level rate limits.
- Introspection or error output exposes unintended schema detail.
- Query depth, breadth, recursion, and resolver cost are unbounded.
- DataLoader or cache keys omit tenant context.
Inventory the schema and every resolver. Do not treat /graphql as one endpoint.
7. WebSocket Handlers
WebSocket authentication usually happens during the connection or first message, but authorization must also happen for every action and subscription.
io.use(authenticateSocket);
io.on('connection', socket => {
socket.on('subscribe-project', async projectId => {
socket.join(`project:${projectId}`);
});
});
The socket is authenticated, but any authenticated user can choose any project room.
Safer pattern:
socket.on('subscribe-project', async projectId => {
const allowed = await projects.canRead({
projectId,
userId: socket.user.id,
tenantId: socket.user.tenantId
});
if (!allowed) return socket.emit('error', { code: 'not_found' });
return socket.join(`tenant:${socket.user.tenantId}:project:${projectId}`);
});
Review connection authentication, token refresh/expiry, origin policy, message schema, per-action authorization, room names, tenant-scoped broadcast, message limits, and cleanup after logout or privilege change.
8. Queue Consumers and Event Handlers
Queues move the trust boundary; they do not remove it.
queue.consume('order-paid', async message => {
await orders.markPaid(message.orderId, message.amount);
});
Questions:
- Who can publish to the topic or queue?
- Is message identity or signature verified?
- Is
amountchecked against the authoritative payment record? - Can an old event be replayed?
- Is the event ID unique and consumed atomically?
- Does the consumer enforce the same tenant and state rules as the API?
- Can a dead-letter message be edited and replayed with elevated worker authority?
Workers often have more privilege than the original user. Treat them as potential confused deputies.
9. Find Entry Points
Review every way work enters the application:
- HTTP routes, controllers, filters, and middleware.
- GraphQL queries, mutations, subscriptions, and field resolvers.
- WebSocket connection handlers and message actions.
- RPC and gRPC services, including every streaming mode.
- File upload, import, and archive-processing jobs.
- Queue consumers and event handlers.
- Scheduled tasks and maintenance commands.
- Webhook receivers and outbound webhooks.
- Mobile deep links, exported components, and custom URL schemes.
- Administrative, debug, health, metrics, reflection, and actuator endpoints.
Framework-neutral searches:
rg -n -i 'route|router|controller|endpoint|resolver|handler|websocket|grpc|webhook|consumer|listener|cron|schedule' . \
-g '!node_modules/**' -g '!vendor/**' -g '!dist/**'
Common framework searches:
# Java / Spring
rg -n '@(RequestMapping|GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|MessageMapping)' --glob '*.java'
# .NET
rg -n '\[(HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete|Route|Authorize|AllowAnonymous)' --glob '*.cs'
# Python
rg -n '@(app|router|blueprint)\.(get|post|put|patch|delete|route)|path\(|re_path\(' --glob '*.py'
# JavaScript / TypeScript
rg -n '\.(get|post|put|patch|delete|use)\s*\(|(router|app)\.(ws|all)\s*\(' --glob '*.{js,jsx,ts,tsx}'
# PHP
rg -n 'Route::(get|post|put|patch|delete|any)|\$app->(get|post|put|delete)' --glob '*.php'
# Go
rg -n '\.(GET|POST|PUT|PATCH|DELETE|Handle|HandleFunc)\s*\(' --glob '*.go'
# Protocol Buffers / gRPC
rg -n '^\s*(service|rpc)\s+' --glob '*.proto'
# GraphQL schema
rg -n '^\s*(type\s+(Query|Mutation|Subscription)|extend\s+type)' --glob '*.{graphql,gql}'
10. Build an Entry-Point Inventory
For each entry point, record:
| Field | Example |
|---|---|
| Protocol | HTTP / gRPC / WebSocket / queue |
| Method / event | POST, Export, subscribe-project, order-paid |
| Path / action | /api/v1/invoices/{id}/approve |
| Handler | InvoiceController.approve |
| Authentication | Bearer token / mTLS / signed event |
| Required role | Finance approver |
| Object / tenant check | Invoice belongs to token tenant |
| Inputs | Path ID, JSON body, metadata, message fields |
| Sensitive sinks | Database update, export, outbound event |
| Response | Invoice status or stream |
An authentication annotation or interceptor is not proof of authorization. Follow the call path to the final object access or state change.
11. Map Trust Boundaries
Typical boundaries:
Browser / Mobile App → API Gateway → Application → Database
↘ Queue → Worker
↘ Object Storage
↘ Third-Party API
CI Pull Request → Build Runner → Artifact Registry → Production
At each boundary, record:
- Identity presented.
- Data accepted.
- Validation performed.
- Authorization decision.
- Encryption and integrity protection.
- Replay and retry behavior.
- Failure behavior.
12. Identify High-Risk Features
| Feature | Why it matters |
|---|---|
| Login, reset, MFA | Account takeover |
| Admin functions | Privilege escalation |
| Tenant-scoped data | Cross-customer exposure |
| Payment, wallet, coupon, refund | Financial abuse |
| File upload / import | Stored payloads, parser bugs, path abuse |
| PDF / image / document conversion | Command execution and SSRF sinks |
| URL fetch / preview / callback | SSRF and credential exposure |
| Search / reporting | Query injection and bulk data access |
| Export / backup | Sensitive data extraction |
| Webhooks | Forged events and replay |
| Template editor | SSTI and stored XSS |
| Serialization / cache | Object injection and tenant mixing |
| Debug / diagnostics | Secrets and privileged operations |
rg -n -i 'admin|reset|mfa|otp|payment|refund|coupon|wallet|upload|import|export|backup|preview|fetch|callback|webhook|template|deserialize|debug|actuator' . \
-g '!node_modules/**' -g '!vendor/**'
13. Map Privilege Changes
Search for code that assigns roles, changes ownership, creates API keys, impersonates users, or changes account state.
rg -n -i 'role|permission|privilege|isAdmin|impersonat|owner(Id)?|tenant(Id)?|api.?key|activate|disable|approve' . \
-g '!node_modules/**' -g '!vendor/**'
Check whether the user can control both the target object and the new privileged value.
14. Map External Calls
Record every outbound integration:
- Destination and whether it is fixed or user-controlled.
- Authentication method and secret storage.
- TLS verification.
- Timeout, redirect, and retry behavior.
- Data sent outside the trust boundary.
- Whether the response is treated as trusted.
rg -n -i 'http(client)?|fetch\(|axios|requests\.|urlopen|resttemplate|webclient|httpclient|curl_exec|net/http' . \
-g '!node_modules/**' -g '!vendor/**'
15. Architecture Questions
- Where is identity created, and where is it consumed?
- Which service is the source of truth for price, role, ownership, and state?
- Is tenant context taken from the authenticated identity or request input?
- Which internal services trust network location instead of identity?
- Can queues, jobs, webhooks, or RPC methods bypass normal route middleware?
- Are old API versions protected by the same controls?
- Are GraphQL fields and WebSocket actions authorized individually?
- Are streaming RPC messages authorized individually?
- Can background workers process data that the API would reject?
- Can one controller or service be called from both protected and unprotected entry points?
Module 4: Data Flow — Sources, Sinks & Controls
Most code-review findings are a controllable source reaching a dangerous sink through an incomplete control.
1. Common Sources
| Source | Examples |
|---|---|
| HTTP | Query, path, body, headers, cookies, multipart filenames |
| Real-time | WebSocket messages, topics, room IDs |
| Files | CSV cells, archive entries, image metadata, document XML |
| Persistence | Database rows originally created by users |
| Messaging | Queue payloads, event attributes, dead-letter replays |
| Integrations | Webhook bodies, OAuth claims, third-party API responses |
| Runtime | Environment variables, CLI arguments, feature flags |
| Client state | Hidden fields, local storage, mobile intents, deep links |
Treat stored data as untrusted when a less-trusted user or system could have written it earlier. This is the second-order vulnerability rule.
2. Common Sinks
| Sink | Risk |
|---|---|
| SQL / NoSQL / LDAP query | Injection, data exposure, authentication bypass |
| Shell / process execution | Remote code execution |
| Template evaluation | Server-side template injection |
| HTML / JavaScript / CSS / URL output | Cross-site scripting |
| File open / write / delete | Traversal, overwrite, arbitrary file access |
| HTTP client / URL fetch | Server-side request forgery |
| Object deserialization | Code execution or logic abuse |
| Redirect / response header | Phishing or header injection |
| Cryptographic API | Weak protection, predictable tokens |
| Authorization query | IDOR, tenant escape, privilege escalation |
| Logging / analytics | Sensitive-data leakage or log injection |
3. Forward Tracing
Start from attacker input and follow it toward sensitive operations.
request.body.reportUrl
→ ReportRequest.url
→ ReportService.createPreview()
→ HttpClient.get(url)
→ internal metadata endpoint reachable
Use forward tracing when reviewing a new endpoint or feature.
4. Backward Tracing
Start at a dangerous sink and work backward to determine who controls its arguments.
Runtime.exec(command)
← PdfConverter.run(command)
← filename appended to command string
← multipart upload original filename
Use backward tracing when a code search finds a small number of high-impact sinks.
5. Verify the Control, Not Its Name
A function named sanitize, validate, or isAllowed may be incomplete. Read it.
Ask:
- Is the control allowlist-based or only removing known bad characters?
- Does decoding happen before or after validation?
- Is canonicalization performed once?
- Is the control appropriate for the final sink?
- Can alternate encodings, aliases, redirects, or wrappers bypass it?
- Is every path protected, including error and background paths?
6. Simple Source-to-Sink Example
Vulnerable Python:
@app.get('/users')
def users():
name = request.args.get('name', '')
return db.execute("SELECT id, name FROM users WHERE name = '" + name + "'")
Safe pattern:
@app.get('/users')
def users():
name = request.args.get('name', '')
return db.execute(
"SELECT id, name FROM users WHERE name = ?",
(name,)
)
The finding is not "a plus operator exists." The finding is "an unauthenticated query parameter reaches a SQL execution sink as query structure."
7. Trace Through Wrappers
Security-sensitive operations are often hidden behind helpers:
controller → service → repository → query builder → database driver
route → utility → shell wrapper → process API
consumer → document service → URL helper → HTTP client
Search for wrapper definitions and all callers:
rg -n 'function runQuery|def run_query|class .*Repository|executeQuery|execCommand|fetchUrl' .
rg -n 'runQuery\(|run_query\(|execCommand\(|fetchUrl\(' .
8. Review Worksheet
Feature / endpoint:
Attacker role:
Source:
Transformations:
Validation:
Authentication:
Authorization:
Sink:
Alternate path:
Impact:
Evidence:
Root cause:
Recommended fix:
9. Confirmation Rules
- Show the source and why it is attacker-controlled.
- Show the sink and why it is security-sensitive.
- Explain every relevant transformation and control.
- Confirm the path is reachable in the reviewed build.
- Prefer a safe unit test or non-destructive proof over speculation.
- Remove real secrets and personal data from evidence.
Module 5: Secrets, Configuration & Environment Review
Configuration decides which security controls actually exist in production.
1. Find Hardcoded Secrets
Search for high-signal names first:
rg -n -i '(api[_-]?key|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd|connection[_-]?string)\s*[:=]' . \
-g '!node_modules/**' -g '!vendor/**' -g '!dist/**' -g '!*.min.js'
rg -n 'BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}' . \
-g '!node_modules/**' -g '!vendor/**'
Then run a secret scanner and manually verify results:
# Scan Git history
gitleaks git --no-banner .
trufflehog git "file://$(pwd)" --results=verified,unknown
# Scan only the current directory contents with Gitleaks
gitleaks dir --no-banner .
TruffleHog's provider verification may contact external services. Use verification only when outbound validation is authorized, and never expose a discovered credential while testing it.
Do not place a discovered secret in a command line, screenshot, issue title, or report. Revoke and rotate confirmed live credentials through the approved process.
2. Search Git History
Deleting a secret from the current branch does not remove it from history.
git log --all --oneline -- '*env*' '*config*' '*secret*' '*credential*'
git log -S 'client_secret' --all --oneline
git log -G '(password|api.?key|private.?key)' --all --oneline -i
Inspect suspicious commits without checking them out:
git show COMMIT:path/to/file
git diff BEFORE_COMMIT..AFTER_COMMIT -- path/to/file
3. Configuration Files
find . -type f \( \
-name '*.env*' -o -name '*config*.yml' -o -name '*config*.yaml' \
-o -name '*config*.json' -o -name 'application*.properties' \
-o -name 'appsettings*.json' -o -name 'web.config' \
\) -not -path '*/node_modules/*' -not -path '*/vendor/*' -print
Check for:
- Development credentials used as production fallbacks.
- Debug mode, verbose errors, or test endpoints enabled outside development.
- TLS verification disabled.
- Wildcard origins or trusted hosts.
- Default encryption keys or JWT secrets.
- Anonymous cloud storage or message queues.
- Insecure cookie defaults.
- Permissive feature flags.
- Administrative interfaces bound to public addresses.
rg -n -i 'debug\s*[:=]\s*true|verify(_ssl)?\s*[:=]\s*false|rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true|allow.?all|0\.0\.0\.0' . \
-g '!node_modules/**' -g '!vendor/**'
4. Dangerous Fallbacks
Vulnerable pattern:
const jwtSecret = process.env.JWT_SECRET || 'change-me';
Safer pattern:
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error('JWT_SECRET is required');
}
Security-critical configuration should fail closed. A missing secret must stop startup, not enable a predictable default.
5. Client-Side Secrets
Anything shipped to a browser or mobile app should be treated as public:
- JavaScript bundles and source maps.
- Android resources, assets,
BuildConfig, and native libraries. - iOS property lists, entitlements, and bundled frameworks.
- Desktop application configuration.
A public API identifier may be acceptable. A credential granting server-side authority is not.
6. Secret Exposure Through URLs and Logs
Check for tokens in:
- Query strings, which may enter browser history, proxy logs, and referrers.
- Exception messages and stack traces.
- Debug logging of request headers or bodies.
- Analytics events and crash reports.
- Health endpoints and configuration dumps.
rg -n -i 'log(ger)?\.(debug|info|warn|error).*?(token|password|authorization|cookie|secret)|print\(.*?(token|password|secret)' .
7. Remediation
- Store secrets in an approved secret manager or protected runtime variable.
- Use short-lived, least-privileged credentials where supported.
- Separate credentials by environment and service.
- Rotate exposed secrets; code deletion alone is insufficient.
- Prevent secrets from entering logs and client artifacts.
- Add pre-commit and CI secret scanning.
- Make insecure or missing production configuration fail closed.
Module 6: Authentication & Session Management
Authentication proves identity; session handling preserves it. Review both as one continuous flow.
1. Map Every Authentication Path
Inventory:
- Password login.
- Password reset and account recovery.
- Registration and email or phone verification.
- MFA enrollment, challenge, recovery, and removal.
- SSO, OAuth 2.0, and OpenID Connect callbacks.
- API keys, service accounts, signed requests, and mutual TLS.
- Mobile biometrics and device binding.
- Impersonation and support access.
- Refresh, logout, revocation, and account deletion.
rg -n -i 'login|signin|authenticate|password.?reset|forgot.?password|mfa|2fa|otp|totp|oauth|oidc|saml|refresh.?token|logout|revoke|impersonat' . \
-g '!node_modules/**' -g '!vendor/**'
2. Password Handling
Verify that:
- Passwords are hashed with a password-hashing function such as Argon2id, scrypt, bcrypt, or PBKDF2 with an appropriate current work factor.
- Every password receives a unique salt; application-wide peppers, if used, are protected separately.
- Password comparison uses the library's safe verification function.
- Passwords are never logged, returned, emailed, or stored reversibly.
- Login and recovery responses do not reveal whether an account exists.
- Password change requires appropriate reauthentication and revokes relevant sessions.
- Default, temporary, and migrated passwords cannot remain valid indefinitely.
High-signal searches:
rg -n -i '(md5|sha1|sha256|sha512)\s*\(.*password|password.*(encrypt|decrypt)|plain.?text.?password' .
rg -n -i 'bcrypt|argon2|scrypt|pbkdf2|password_hash|PasswordHasher' .
A general-purpose hash such as raw SHA-256 is not a password-hashing function.
3. Login Logic
Follow the complete decision:
Identifier normalization
→ account lookup
→ account state check
→ password verification
→ MFA decision
→ session/token creation
→ audit event
Check for:
- Fail-open exception handling.
- Disabled, locked, deleted, or unverified accounts that can still authenticate.
- Alternate endpoints that skip MFA.
- Case or Unicode differences that select the wrong account.
- LDAP or SQL injection in the account lookup.
- Rate limiting applied only by IP and easily distributed.
- Password validation occurring after a session or token is created.
4. Password Reset and Recovery
Reset tokens should be random, single-use, short-lived, and bound to one account and one purpose.
Review:
- Token generation uses a cryptographically secure random source.
- Only a hash of the token is stored when practical.
- Expiry and used-state are checked atomically.
- Generating a new token invalidates older tokens if required by the design.
- The reset link host is not built from an untrusted
Hostor forwarded header. - Reset does not change a different account through a user-controlled ID.
- Successful reset revokes active sessions or gives the user that option.
Vulnerable host construction:
const link = `https://${req.headers.host}/reset?token=${token}`;
Safer pattern:
const link = `${config.publicBaseUrl}/reset?token=${token}`;
5. Multi-Factor Authentication
Check the entire lifecycle:
- Enrollment requires a recently authenticated session.
- The server generates TOTP secrets with a secure random source.
- OTP and recovery codes have attempt limits and expiry where applicable.
- Recovery codes are hashed and single-use.
- Disabling or replacing MFA requires strong reauthentication.
- Backup flows do not silently reduce assurance.
- "Remember this device" tokens are signed, scoped, expiring, and revocable.
- MFA is enforced after every authentication path, including SSO linking and API login.
6. Session Cookies
Expected cookie properties for a browser session:
Secure; HttpOnly; SameSite=Lax or SameSite=Strict; narrow Domain; narrow Path
Also verify:
- Session ID rotates after login, privilege change, and impersonation.
- Logout invalidates server-side session state, not only the browser cookie.
- Absolute and idle timeouts are enforced server-side.
- Concurrent-session behavior matches the risk model.
- Session storage cannot mix users or tenants through an incomplete cache key.
- State-changing requests have CSRF protection when cookies authenticate them.
rg -n -i 'setCookie|res\.cookie|SameSite|HttpOnly|SecureCookie|session.?timeout|session\.regenerate|invalidate\(' .
7. JSON Web Tokens
Never treat token decoding as token verification.
Vulnerable:
const claims = jwt.decode(req.headers.authorization.slice(7));
req.user = claims;
Safer pattern:
const claims = jwt.verify(token, verificationKey, {
algorithms: ['RS256'],
issuer: 'https://identity.example.com/',
audience: 'payments-api'
});
Verify:
- Signature validation is mandatory.
- Allowed algorithms are explicit;
noneand algorithm confusion are rejected. iss,aud,exp, andnbfare validated where applicable.- The key is selected safely; attacker-controlled
kid,jku, orx5ucannot cause arbitrary key retrieval or path access. - Access and refresh tokens have separate purpose, audience, lifetime, and storage rules.
- Revocation or rotation exists for long-lived tokens.
- Authorization does not trust a role or tenant claim that the application itself lets the user edit.
8. OAuth 2.0 and OpenID Connect
Check:
- Redirect URIs are exact allowlisted values, not substring or suffix matches.
statebinds the authorization response to the initiating browser session.- OIDC
nonceis generated and verified. - Authorization Code with PKCE is used for public clients.
- The authorization code is single-use and exchanged only by the intended client.
- ID token signature, issuer, audience, time claims, and nonce are verified.
- Account linking requires proof that both identities belong to the same user.
- Tokens are not placed in URLs or exposed to unrelated origins.
9. API Keys and Signed Requests
- Generate keys with a cryptographically secure random source.
- Show a secret once and store only a verifier when possible.
- Prefix keys with a non-secret identifier to support lookup and rotation.
- Scope keys to required operations and tenants.
- Support expiry, revocation, and last-used auditing.
- For signed webhooks, verify the signature over the raw received bytes, include a timestamp, enforce a replay window, and compare signatures in constant time.
Hardening
- Centralize authentication and token verification.
- Fail closed on parsing, key lookup, and identity-provider errors.
- Apply rate limits by account and relevant network or device signals.
- Rotate sessions after changes in authentication strength or privilege.
- Test recovery and linking paths as carefully as primary login.
- Log security events without storing credentials or tokens.
Module 7: Authorization, IDOR & Tenant Isolation
A valid identity is not permission. Verify every action against the target resource and current tenant.
Key Concepts
Function-Level Authorization — Whether the caller may perform the operation, such as approving an invoice or creating an administrator.
Object-Level Authorization — Whether the caller may access this specific invoice, file, message, or user record.
Property-Level Authorization — Whether the caller may read or modify a sensitive field such as role, balance, ownerId, or isApproved.
Tenant Isolation — Assurance that one customer, organization, or workspace cannot access another tenant's data or actions.
IDOR / BOLA — Direct object access based on a user-controlled identifier without an adequate object-level authorization check.
1. Review the Authorization Model
Document:
| Role | Allowed actions | Object scope | Sensitive restrictions |
|---|---|---|---|
| Anonymous | Register, login | None | No account data |
| User | Read/update own profile | Own user ID | Cannot set role or tenant |
| Manager | Approve team requests | Assigned team | Cannot approve own request |
| Tenant admin | Manage tenant users | Current tenant | Cannot access platform admin |
| Platform admin | Cross-tenant support | Approved workflow | Fully audited |
If no clear model exists, missing authorization is likely to be inconsistent.
2. Trace Object Access
Vulnerable pattern:
@GetMapping("/invoices/{id}")
Invoice getInvoice(@PathVariable UUID id) {
return invoiceRepository.findById(id).orElseThrow();
}
Safer pattern:
@GetMapping("/invoices/{id}")
Invoice getInvoice(@PathVariable UUID id, AuthenticatedUser user) {
return invoiceRepository
.findByIdAndTenantId(id, user.tenantId())
.orElseThrow(NotFoundException::new);
}
Prefer constraining the database query by tenant and ownership rather than fetching a global object and checking later.
3. Search for Identifier-Based Access
rg -n -i 'findById|getById|load\(|findOne|findUnique|findFirst|where.*id|params\..*id|path.*id' . \
-g '!node_modules/**' -g '!vendor/**'
rg -n -i 'tenant(Id)?|org(anization)?Id|workspaceId|accountId|ownerId|createdBy' . \
-g '!node_modules/**' -g '!vendor/**'
For each lookup, identify where the tenant or ownership constraint originates. It should come from trusted server-side identity context, not from a request parameter alone.
4. Horizontal and Vertical Tests
For each sensitive endpoint, reason through:
Same role, different user
Same role, different tenant
Lower role, same object
Lower role, guessed object ID
Anonymous caller
Disabled or stale account
Background job or alternate API version
Unpredictable UUIDs reduce guessing; they do not replace authorization.
5. Mass Assignment
Vulnerable:
await User.update(req.body, { where: { id: req.user.id } });
The body may include role, tenantId, emailVerified, or creditLimit.
Safer pattern:
const input = {
displayName: req.body.displayName,
timezone: req.body.timezone
};
await User.update(input, { where: { id: req.user.id } });
Use explicit input models and explicit field mapping. Do not bind transport objects directly to privileged persistence models.
6. Authorization Middleware and Annotations
Search for both protected and intentionally unprotected routes:
rg -n -i 'authorize|permission|policy|guard|preauthorize|rolesallowed|allowanonymous|permitall|skip.?auth|publicRoute' .
Check:
- Middleware order: authentication must run before authorization and the handler.
- Route groups: a child route may escape parent middleware.
- Annotations: overrides and inheritance may behave unexpectedly.
- Internal calls: service methods may assume the controller already checked access.
- Background jobs: user identity and tenant context may be lost.
- GraphQL: authorization must protect resolvers and fields, not only the endpoint.
- WebSockets: authenticate the connection and authorize every message action and room subscription.
7. Multi-Tenant Data Access
High-risk patterns:
SELECT * FROM invoices WHERE id = :id
cache key = invoice:{id}
object storage key = uploads/{filename}
queue message = { invoiceId } without tenantId
search index query without tenant filter
Safer patterns bind tenant identity throughout:
SELECT * FROM invoices WHERE tenant_id = :trustedTenant AND id = :id
cache key = tenant:{trustedTenant}:invoice:{id}
object storage key = tenants/{trustedTenant}/uploads/{serverGeneratedId}
Verify tenant isolation in databases, caches, search indexes, object storage, queues, exports, analytics, and error messages.
8. Privileged Operations
Pay special attention to:
- Role and permission changes.
- User invitation and account linking.
- Ownership transfer.
- API-key creation and rotation.
- Export, backup, impersonation, and support tooling.
- Refund, payout, approval, and override actions.
- Feature-flag changes.
Require appropriate reauthentication, separation of duties, and audit events where risk justifies it.
Hardening
- Deny by default and centralize reusable policy decisions.
- Enforce authorization server-side at the object access or state-change boundary.
- Derive tenant context from verified identity.
- Use explicit input and output models to prevent property-level exposure.
- Test every role against every sensitive action and object scope.
- Add negative authorization tests to CI.
Module 8: Injection Vulnerabilities
Injection occurs when data is interpreted as instructions by a downstream language or engine.
1. Injection Review Table
| Type | Common source | Dangerous sink | Primary control |
|---|---|---|---|
| SQL injection | Request or stored value | Raw SQL execution | Parameterized query |
| NoSQL injection | JSON object or query | Operator-based query | Typed schema and operator rejection |
| OS command injection | Filename, host, option | Shell or process API | Avoid shell; fixed executable and arguments |
| LDAP injection | Username, search filter | LDAP filter | Library escaping and strict input rules |
| Template injection | Email or page template | Server template engine | Do not evaluate user templates; sandbox if unavoidable |
| Expression injection | Rule or filter input | SpEL, OGNL, EL, eval | Fixed grammar and safe parser |
| Header injection | Name, URL, filename | Response or email header | Reject CR/LF and use safe APIs |
| Log injection | Any displayed value | Plain-text log or terminal | Structured logging and control-character handling |
2. SQL Injection
Search for raw query construction:
rg -n -i '(select|insert|update|delete).*?(\+|format\(|f"|\$\{|sprintf)|execute(Query|Update)?\s*\(|raw(Query)?\s*\(|FromSqlRaw|createNativeQuery' . \
-g '!node_modules/**' -g '!vendor/**'
Vulnerable Java:
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
return jdbcTemplate.queryForList(sql);
Safer pattern:
String sql = "SELECT * FROM users WHERE email = ?";
return jdbcTemplate.queryForList(sql, email);
Check more than WHERE values:
- Dynamic table or column names.
ORDER BYdirection and field.LIMITor pagination expressions.- Full-text search syntax.
- Stored procedures that build dynamic SQL.
- Second-order input read from the database and later placed in a query.
Identifiers usually cannot be parameterized. Map them through a strict server-side allowlist:
allowed_sort = {'name': 'name', 'created': 'created_at'}
column = allowed_sort.get(request.args.get('sort'), 'created_at')
query = f'SELECT id, name FROM users ORDER BY {column}'
3. NoSQL Injection
Vulnerable MongoDB-style query:
const user = await users.findOne({
username: req.body.username,
password: req.body.password
});
If the body parser accepts objects, values such as { "$ne": null } may change query meaning.
Safer approach:
const schema = z.object({
username: z.string().min(1).max(100),
password: z.string().min(1).max(200)
});
const input = schema.parse(req.body);
Then verify a password hash separately. Reject unexpected object types and query operators from untrusted input.
4. OS Command Injection
Search:
rg -n -i 'Runtime\.getRuntime\(\)\.exec|ProcessBuilder|Process\.Start|child_process|execSync|spawn\(|subprocess\.|os\.system|shell_exec|passthru|popen\(' .
Vulnerable Python:
subprocess.run(f"convert {filename} output.png", shell=True)
Safer pattern:
subprocess.run(
['/usr/bin/convert', safe_input_path, safe_output_path],
shell=False,
check=True,
timeout=10
)
Also check:
- User-controlled executable paths.
- Arguments beginning with
-that become options. - Environment variables such as
PATH. - Working directories and output paths.
- Wrapper scripts that reintroduce a shell.
- Image, PDF, media, and archive utilities.
5. LDAP Injection
Vulnerable:
String filter = "(&(uid=" + username + ")(active=true))";
ldapTemplate.search("ou=people", filter, mapper);
Use the LDAP library's filter-escaping or parameterized filter-building API. Distinguished-name escaping and search-filter escaping are different contexts.
6. Server-Side Template Injection
Search for runtime template creation:
rg -n -i 'render_template_string|Template\(|from_string|createTemplate|process\(|evaluate\(|parseExpression|thymeleaf|freemarker|velocity|twig' .
Review whether users can control the template itself, not only data passed into a fixed template.
# Dangerous: user input becomes template syntax
return render_template_string(request.form['welcome'])
# Safer: fixed template, user input is data
return render_template('welcome.html', welcome=request.form['welcome'])
Sandboxing template engines is difficult. Prefer fixed templates and constrained placeholders.
7. Expression and Code Evaluation
rg -n -i '\beval\s*\(|\bexec\s*\(|compile\s*\(|ScriptEngine|parseExpression|Expression\.Lambda|new Function\s*\(' .
Do not pass untrusted rules, filters, formulas, or serialized code to a general-purpose evaluator. Implement a small grammar with an allowlisted parser.
8. Proving Injection Safely
- Prefer a unit or integration test using a non-destructive marker.
- Demonstrate changed query structure or controlled delay only in an authorized test environment.
- Do not extract unrelated records when a boolean difference is sufficient.
- For command injection, use a harmless controlled output or mocked process call.
- Record the exact source, sink, and missing control.
Hardening
- Keep data separate from instructions through parameterized APIs.
- Use fixed executable paths and argument arrays; avoid shells.
- Parse input into strict scalar types and reject unexpected structures.
- Use context-specific escaping only when parameterization is unavailable.
- Replace general-purpose evaluators with constrained parsers.
- Add regression tests for the exact vulnerable data flow.
Module 9: Browser-Side Security
Review the output context, browser trust boundary, and credential transport together.
1. Cross-Site Scripting Contexts
The correct control depends on where data is inserted:
| Output context | Safer approach |
|---|---|
| HTML text | HTML entity encoding |
| HTML attribute | Attribute encoding and quoted values |
| JavaScript value | JSON serialization into a safe data channel |
| URL parameter | URL encoding plus scheme/host validation |
| CSS | Avoid untrusted values; use strict allowlists |
| Rich HTML | Maintained HTML sanitizer with a minimal policy |
Encoding for one context does not make data safe in another.
2. Search for Dangerous Browser Sinks
rg -n 'innerHTML|outerHTML|insertAdjacentHTML|document\.write|\.html\(|dangerouslySetInnerHTML|v-html|\[innerHTML\]|bypassSecurityTrust|eval\(|new Function\(' \
--glob '*.{js,jsx,ts,tsx,vue,html}'
Vulnerable React:
<div dangerouslySetInnerHTML={{ __html: profile.bio }} />
Safer for plain text:
<div>{profile.bio}</div>
If rich HTML is a real requirement, sanitize it using a maintained library with a narrow policy immediately before the sink.
3. Stored and Second-Order XSS
Trace data written by one user and rendered to another:
- Profile fields shown to administrators.
- Ticket descriptions and chat messages.
- Uploaded SVG or HTML files.
- Audit logs and dashboard labels.
- Email templates and notification previews.
- Filenames and document metadata.
The original write endpoint may validate data differently from the later rendering context.
4. URL and Navigation Security
Review redirects, callback targets, links, iframe sources, and scheme handlers.
rg -n -i 'redirect\(|location\.(href|assign|replace)|window\.open|returnUrl|redirectUri|callbackUrl|nextUrl' .
Avoid checks such as:
if (url.includes('example.com')) redirect(url);
Prefer relative paths or parse the URL and compare normalized scheme, host, and port against an exact allowlist.
Reject dangerous schemes such as javascript: and unexpected user-info, encoded delimiters, or protocol-relative URLs.
5. Cross-Site Request Forgery
CSRF is relevant when the browser automatically attaches authentication, normally cookies or client certificates.
Verify:
- State-changing operations do not use
GET. - A framework CSRF token or equivalent origin-bound defense is enabled.
- Token comparison is server-side and session-bound.
SameSitecookies support the design but are not the only defense for high-risk flows.- Login, email change, password change, MFA, and payment actions are covered.
- JSON endpoints do not accept alternate simple content types that bypass expected preflight behavior.
rg -n -i 'csrf|xsrf|SameSite|disable\(.*csrf|csrf.*disable|ignore.*csrf' .
6. Cross-Origin Resource Sharing
Check the actual origin-matching logic.
High-risk patterns:
- Reflecting any
Originvalue. Access-Control-Allow-Credentials: truewith an overly broad origin policy.- Suffix checks that allow
example.com.attacker.tld. - Prefix checks that allow
https://example.com.attacker.tld. - Trusting
nullorigin without a specific requirement. - Broad development origins enabled in production.
rg -n -i 'cors|allowedOrigins|Access-Control-Allow-Origin|supports_credentials|AllowCredentials' .
7. Content Security Policy
CSP is defense in depth, not a replacement for output encoding or sanitization.
Review for:
- Broad sources such as
*. unsafe-inlineorunsafe-evalwithout a justified migration plan.- User-controlled script hosts.
- Missing nonces or hashes where strict policies are intended.
- Report-only policy mistakenly treated as enforcement.
- Clickjacking protection through
frame-ancestorsor equivalent headers.
8. Client Storage and Messaging
- Do not store long-lived sensitive tokens in browser storage when an
HttpOnlycookie design is available. - Validate
postMessagesender origin with exact comparison and validate message structure. - Never send secrets to
*target origin. - Treat service-worker caches, IndexedDB, and offline files as persistent client storage.
- Clear sensitive state during logout when appropriate.
rg -n 'localStorage|sessionStorage|indexedDB|postMessage|addEventListener\([^)]*message' --glob '*.{js,jsx,ts,tsx}'
Hardening
- Use framework auto-escaping and avoid escape hatches.
- Apply context-specific output handling at the final sink.
- Use CSRF defenses for cookie-authenticated state changes.
- Compare normalized origins and redirect destinations against exact allowlists.
- Keep sensitive credentials out of browser-readable storage where possible.
- Add CSP as a narrow, tested secondary control.
Module 10: SSRF, File Handling, Parsers & Deserialization
Features that fetch, open, convert, extract, or deserialize data expose powerful server-side capabilities.
1. Server-Side Request Forgery
Common SSRF features:
- URL preview and screenshot generation.
- Import from URL.
- PDF, image, or document conversion.
- Webhook testing and callback validation.
- Avatar or attachment download.
- SSO discovery and metadata loading.
- Proxy, feed, or health-check endpoints.
rg -n -i 'fetch\(|axios\.|requests\.|urlopen|HttpClient|RestTemplate|WebClient|curl_exec|http\.Get|download.*url|preview.*url|callback.*url' .
Trace the final resolved destination, not only the original string.
Check:
- Allowed schemes are limited to those required, usually
https. - URLs are parsed by one well-understood parser.
- Authentication information in URLs is rejected.
- DNS resolution and every connected IP are checked.
- Loopback, private, link-local, multicast, reserved, and internal ranges are blocked when not required.
- IPv4, IPv6, integer, octal-like, mapped, and encoded representations are handled consistently.
- Redirects are disabled or every redirect target is revalidated.
- Proxy configuration cannot provide an internal bypass.
- Response size, content type, and timeout are limited.
- Cloud metadata and internal control-plane endpoints are unreachable.
Best control: fetch only known destinations using server-side identifiers mapped to fixed URLs.
2. Path Traversal
Search for file paths built from input:
rg -n -i 'open\(|readFile|writeFile|sendFile|File(Input|Output)Stream|Paths\.get|Path\.Combine|filepath\.(Join|Clean)|include\s*\(|require\s*\(' .
Vulnerable Python:
return send_file('/srv/reports/' + request.args['name'])
Safer pattern:
base = Path('/srv/reports').resolve()
candidate = (base / request.args['name']).resolve()
if base not in candidate.parents:
abort(404)
return send_file(candidate)
Also reject absolute paths, platform-specific separators, alternate encodings, symlink escapes, and filename confusion. Where possible, map an opaque record ID to a server-stored path instead of accepting a path.
3. File Upload
Review the complete lifecycle:
multipart input → validation → temporary storage → transformation → permanent storage → retrieval / rendering → deletion
Check:
- Authentication, authorization, ownership, and quota.
- Server-generated storage names.
- Size limits enforced during streaming, not only after buffering.
- Extension, MIME type, file signature, and actual parser behavior.
- Active content such as HTML, SVG, macro-enabled documents, and polyglots.
- Storage outside executable or publicly served paths.
- Separate trusted origin or forced download for user content.
- Image or document libraries patched and sandboxed where practical.
- Metadata removal where privacy requires it.
- Archive expansion limits.
- Deletion and access checks use trusted object identity.
Do not trust the multipart Content-Type or original filename.
4. Archive Extraction and Zip Slip
Each archive entry must remain under the intended extraction directory after normalization.
../../app/config.yml
/etc/cron.d/job
C:\Windows\Temp\payload
symlink → outside extraction root
Also limit:
- Number of entries.
- Total uncompressed size.
- Compression ratio.
- Nested archive depth.
- Processing time.
5. XML External Entity Processing
Search for XML parsers and inspect their actual configuration:
rg -n -i 'DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|XmlReaderSettings|lxml|etree|simplexml_load|DOMDocument|encoding/xml' .
Disable external entities, DTD processing, and external schema resolution unless explicitly required. Use hardened library defaults and tests; security properties differ by parser and version.
6. Unsafe Deserialization
High-risk APIs:
rg -n -i 'ObjectInputStream|BinaryFormatter|LosFormatter|NetDataContractSerializer|pickle\.loads?|yaml\.load\(|unserialize\(|Marshal\.load|gob\.NewDecoder|readObject\(' .
Questions:
- Can untrusted data reach the deserializer?
- Can a signature or encryption key be obtained or reused by an attacker?
- Are polymorphic types or type metadata enabled?
- Does the classpath contain gadget-capable libraries?
- Is deserialized state used for authorization, price, role, or workflow decisions?
Prefer simple data formats mapped into explicit schemas. A signature protects integrity only if the key is secret and the design prevents replay and cross-purpose use.
7. Server-Side Includes and Dynamic Loading
Review:
- Dynamic
include,require, reflection, plugin loading, and class loading. - Template names or view paths derived from request input.
- Native library and DLL search paths.
- User-controlled archive or package installation.
Map user choices to fixed server-side identifiers. Do not construct executable module paths from input.
8. Parser Abuse
Parsers may be safe from code execution but still vulnerable to resource exhaustion or logic confusion.
Check limits for:
- JSON depth, keys, array length, and numeric range.
- CSV row and cell length, formulas, and encoding.
- YAML aliases and type construction.
- Regex backtracking on attacker-controlled text.
- GraphQL depth, aliases, batching, and field cost.
- Multipart field count and boundary length.
Hardening
- Replace user-provided paths and destinations with server-side IDs where possible.
- Canonicalize once, then enforce containment or an exact destination allowlist.
- Revalidate every SSRF redirect and resolved address.
- Store uploads with server-generated names outside executable paths.
- Disable dangerous parser features and set strict resource limits.
- Deserialize only explicit data schemas, never arbitrary objects from untrusted sources.
Module 11: Cryptography, Sensitive Data & Logging
Cryptography protects data only when the algorithm, key lifecycle, purpose, and surrounding logic are all correct.
1. Classify Sensitive Data
Identify where the application handles:
- Passwords, recovery codes, API keys, and session tokens.
- Personal, financial, health, or location data.
- Private messages, documents, and uploaded files.
- Signing keys, encryption keys, certificates, and seeds.
- Authentication claims and device identifiers.
- Internal system details and security events.
Then trace the full lifecycle:
Collection → Validation → Use → Storage → Logging → Export → Backup → Deletion
2. Find Cryptographic Operations
rg -n -i 'cipher|encrypt|decrypt|sign|verify|digest|hash|hmac|random|nonce|iv|salt|keystore|keychain|secretkey|privatekey|certificate' . \
-g '!node_modules/**' -g '!vendor/**'
Look for obsolete or easily misused primitives:
rg -n -i '\b(DES|3DES|RC2|RC4|ECB|MD5|SHA-?1)\b|Math\.random|java\.util\.Random|random\.random|rand\(\)' . \
-g '!node_modules/**' -g '!vendor/**'
The presence of an old hash may be acceptable for a non-security checksum. Confirm its purpose before reporting it.
3. Encryption Review
Verify:
- A modern authenticated-encryption mode is used, such as AES-GCM or ChaCha20-Poly1305.
- Nonces are unique for a given key and generated according to the chosen algorithm's requirements.
- Authentication tags are verified before plaintext is used.
- Ciphertext includes a version or key identifier needed for safe rotation.
- Additional authenticated data binds important context such as tenant, record type, or protocol version.
- Encryption keys are not stored beside ciphertext with equivalent access.
- Decryption errors fail closed and do not become a padding or format oracle.
Encryption without integrity protection can allow undetected modification.
4. Key Management
Review:
- How keys are generated.
- Where keys are stored.
- Which identity can access them.
- How environments and tenants are separated.
- How rotation and revocation work.
- Whether old data can be migrated.
- Whether keys appear in logs, backups, crash dumps, or client applications.
Prefer a managed KMS, HSM, operating-system keystore, Android Keystore, or iOS Keychain appropriate to the platform. Do not invent custom key wrapping.
5. Randomness and Tokens
Security tokens must use a cryptographically secure random number generator.
Dangerous examples:
const resetToken = Math.random().toString(36);
token = str(random.randint(100000, 999999))
Safer examples:
const resetToken = crypto.randomBytes(32).toString('base64url');
token = secrets.token_urlsafe(32)
Check entropy, expiry, single-use state, purpose binding, and comparison logic—not only the random API.
6. Transport Security
Search for disabled certificate or hostname verification:
rg -n -i 'verify\s*=\s*false|rejectUnauthorized\s*:\s*false|InsecureSkipVerify\s*:\s*true|TrustAll|ALLOW_ALL_HOSTNAME_VERIFIER|ServerCertificateCustomValidationCallback' .
Verify:
- Certificate and hostname validation remain enabled.
- Internal services authenticate each other where required.
- Cleartext protocols are not used for credentials or sensitive data.
- Redirects do not downgrade from HTTPS.
- Mobile network security configuration does not trust user CAs or permit cleartext in production without a justified scope.
7. Sensitive Data in Logs
Search for request or object logging:
rg -n -i 'log(ger)?\.(trace|debug|info|warn|error)|console\.log|print\(|System\.out\.print|fmt\.Print' . \
-g '!node_modules/**' -g '!vendor/**'
Check whether logs include:
- Passwords, OTPs, reset links, tokens, cookies, or authorization headers.
- Full payment or personal records.
- Request and response bodies.
- Signed URLs or connection strings.
- Cryptographic material.
- Cross-tenant data visible to shared operators.
Prefer structured logging with explicit safe fields. Broad object serialization can expose new sensitive fields after an unrelated model change.
8. Log Injection and Audit Integrity
- Use structured log fields rather than concatenated lines.
- Handle control characters before plain-text log display.
- Include actor, action, target, tenant, result, and correlation ID for security events.
- Obtain actor identity from trusted context, not a request body.
- Protect audit logs from application-user modification.
- Do not let a logging failure approve or repeat a sensitive transaction unexpectedly.
9. Error Handling
External responses should not expose:
- Stack traces and source paths.
- SQL statements or database errors.
- Internal hostnames and service URLs.
- Tokens, secrets, or user records.
- Framework and dependency detail beyond operational need.
Internal logs need enough context for investigation but must still exclude credentials and unnecessary personal data.
rg -n -i 'stacktrace|printStackTrace|includeStacktrace|detailedErrors|UseDeveloperExceptionPage|traceback\.print_exc|display_errors' .
10. Caches, Temporary Files and Backups
Verify that sensitive data is not unintentionally retained in:
- Shared caches with incomplete tenant or user keys.
- Temporary directories with broad permissions.
- Download and export staging folders.
- Build artifacts and container layers.
- Client-side offline caches.
- Database snapshots and support bundles.
Deletion at the primary database is not complete if copies remain in exports, object storage, search indexes, or caches contrary to the retention policy.
Hardening
- Use maintained high-level cryptographic libraries and approved platform primitives.
- Keep keys outside source code and separate from protected data.
- Use CSPRNGs for every security token.
- Preserve TLS certificate and hostname verification.
- Log explicit safe fields and protect audit integrity.
- Apply consistent data classification, retention, and deletion across all storage layers.
Module 12: Business Logic, Race Conditions & Abuse Controls
Business logic findings appear when individually valid operations create an invalid outcome.
1. Model the State Machine
Write the expected states and transitions for high-value workflows.
Order: Created → Payment Pending → Paid → Fulfilled → Refunded
Invite: Created → Sent → Accepted / Expired / Revoked
Approval: Draft → Submitted → Approved / Rejected → Executed
For each transition, ask:
- Who may initiate it?
- Which prior state is required?
- Which server-side evidence proves the transition?
- Can it be repeated, reordered, or skipped?
- Can the actor approve their own action?
- Is the state checked and updated atomically?
2. Never Trust Client-Calculated Authority
High-risk request fields:
price, total, discount, role, ownerId, tenantId, isPaid,
isVerified, approved, status, credit, commission, plan
The server should calculate or retrieve authoritative values.
Vulnerable:
await chargeCard(req.body.total);
await order.update({ status: req.body.status });
Safer pattern:
const order = await loadOrderForUser(req.params.id, req.user.id);
const total = await pricingService.calculate(order.items);
await chargeCard(total);
await markPaidAfterVerifiedCharge(order.id, charge.id);
3. Payment and Webhook Logic
Verify:
- Payment status is confirmed with a trusted provider or valid signed webhook.
- Webhook signature covers the raw body and is checked before parsing-dependent changes.
- Event ID is stored to prevent replay.
- Amount, currency, merchant, order reference, and final provider state match.
- A user cannot call the internal "paid" handler directly.
- Refund and payout amounts are derived from authoritative records.
- Failure and retry paths do not duplicate fulfillment or credit.
Search:
rg -n -i 'webhook|signature|payment|paid|refund|payout|charge|invoice|coupon|discount|wallet|balance' .
4. Race Conditions and TOCTOU
Vulnerable pattern:
1. Read balance = 100
2. Confirm withdrawal 80 is allowed
3. A concurrent request performs the same check
4. Both write a new balance
Review for:
- Read-check-write sequences outside a transaction.
- Missing row locks, compare-and-swap, or optimistic version checks.
- Separate services enforcing a shared quota independently.
- One-time token validation and consumption in separate operations.
- Inventory, coupon, referral, voting, and withdrawal operations.
- Files checked before use but replaceable through links or renames.
Safer database pattern:
UPDATE accounts
SET balance = balance - :amount
WHERE id = :id
AND tenant_id = :tenant
AND balance >= :amount;
Treat zero affected rows as a rejected transaction. The exact concurrency control depends on the database and invariant.
5. Idempotency and Replay
Sensitive requests may be retried by browsers, clients, queues, or providers.
Check:
- A unique idempotency key is bound to the authenticated actor, operation, and normalized payload.
- Reusing a key with a different payload fails.
- The result is stored atomically with the state change.
- Keys expire after an appropriate period.
- Queue consumers deduplicate event IDs.
- Signed requests include a timestamp or nonce and a replay window.
6. Numeric and Boundary Validation
Check quantities, prices, limits, timestamps, and counters for:
- Negative and zero values.
- Integer overflow or truncation.
- Floating-point money calculations.
NaN, infinity, scientific notation, and signed zero.- Currency and unit confusion.
- Time-zone and daylight-saving transitions.
- Maximum batch sizes and pagination values.
- Rounding performed differently across services.
Use integer minor units or a suitable decimal type for money and define one rounding rule.
7. Rate Limiting and Resource Abuse
Rate limits should protect the business operation, not only the route.
Review:
- Login, password reset, OTP, invitations, and verification messages.
- Search, export, report generation, and expensive GraphQL queries.
- File upload, conversion, archive extraction, and URL fetch.
- Coupon, referral, voting, and promotional flows.
- API-key creation, token refresh, and webhook testing.
Check whether limits can be bypassed by changing an IP, case, route version, tenant identifier, or equivalent username format.
8. Workflow Bypass
Look for alternate ways to reach the final action:
- Direct API call instead of UI sequence.
- Old API version.
- Mobile or internal endpoint.
- Background job or queue message.
- Import or bulk endpoint.
- Duplicate parameter or unexpected HTTP method.
- Client-controlled state carried between steps.
9. Common Abuse Cases
| Feature | Abuse question |
|---|---|
| Coupon | Can it be reused, stacked, self-issued, or applied after expiry? |
| Referral | Can one actor create both sides or cycle identities? |
| Approval | Can requester and approver be the same principal? |
| Export | Can filters be removed to retrieve the full dataset? |
| Invitation | Can the recipient, tenant, or role change after issuance? |
| File share | Does revocation invalidate every link and cached permission? |
| Subscription | Can a downgraded plan retain premium state or objects? |
| Refund | Can cumulative refunds exceed captured payment? |
| Wallet | Can concurrent credits or debits violate the balance invariant? |
| Notification | Can content, recipient, or delivery channel be changed independently? |
Hardening
- Define server-side state machines and reject invalid transitions.
- Use authoritative server-side values for identity, tenant, price, role, and status.
- Enforce financial and quota invariants atomically.
- Make retryable operations idempotent.
- Apply abuse controls to the underlying actor and operation.
- Write negative tests for replay, reordering, concurrency, and boundary values.
Module 13: Dependencies, CI/CD & Supply Chain
The reviewed application includes the code it imports and the pipeline that turns source into production.
1. Dependency Inventory
Locate manifests and lockfiles:
find . -type f \( \
-name 'package.json' -o -name 'package-lock.json' -o -name 'yarn.lock' -o -name 'pnpm-lock.yaml' \
-o -name 'requirements*.txt' -o -name 'poetry.lock' -o -name 'Pipfile.lock' \
-o -name 'pom.xml' -o -name 'build.gradle*' -o -name 'gradle.lockfile' \
-o -name '*.csproj' -o -name 'packages.lock.json' \
-o -name 'composer.json' -o -name 'composer.lock' \
-o -name 'go.mod' -o -name 'go.sum' \
-o -name 'Gemfile' -o -name 'Gemfile.lock' \
-o -name 'Cargo.toml' -o -name 'Cargo.lock' \
\) -not -path '*/node_modules/*' -not -path '*/vendor/*' -print
Check:
- A lockfile exists for deployable applications where the ecosystem supports it.
- The lockfile is generated from and consistent with the manifest.
- Production does not resolve broad or floating versions unexpectedly.
- Packages come from approved registries.
- Direct Git, URL, local-path, or pre-release dependencies are justified.
- Abandoned libraries and duplicate major versions are understood.
- Development packages cannot enter production unintentionally.
2. Vulnerability Scanning
Use the native tool where available:
Run package restoration, compilation, and dependency resolution only in an isolated, non-privileged environment when the repository is not fully trusted. These operations may execute package hooks, build scripts, or project-controlled tooling and may send dependency metadata to registries.
# JavaScript / TypeScript
npm audit
# Python
pip-audit
# Java / JVM
mvn org.owasp:dependency-check-maven:13.0.0:check
# .NET 10 SDK and later
dotnet package list --vulnerable --include-transitive
# .NET 9 SDK and earlier
dotnet list package --vulnerable --include-transitive
# PHP
composer audit
# Go
govulncheck ./...
# Ruby
bundle audit check --update
# Rust
cargo audit
Do not report a CVE only because a package appears in a lockfile. Confirm the affected version, vulnerable component, reachable feature, deployment context, and available fix. Reachability can adjust priority; it does not automatically remove supply-chain r
Comments