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:

  1. Who can reach it?
  2. What data can they control?
  3. What security decision is made?
  4. Where is that decision enforced?
  5. 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:

  1. Authentication, password reset, MFA, and token verification.
  2. Authorization and tenant filtering.
  3. Administrative, payment, export, webhook, upload, and integration features.
  4. SQL, OS command, template, file, deserialization, and outbound-request sinks.
  5. Secrets, configuration, dependencies, and CI/CD.
  6. 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:

  • GET is the accepted method.
  • orderId is attacker-controlled path input.
  • requireLogin should establish identity.
  • validateOrderId should validate syntax, not ownership.
  • getOrder is the controller entry point.

What to inspect:

  1. Where is this router mounted? /api, /internal, or more than once?
  2. Does requireLogin return immediately after failure, or can execution continue?
  3. Is a broader route registered first and shadowing this route?
  4. Is the same controller reachable through another route without middleware?
  5. Are HEAD, OPTIONS, method override, alternate content types, and trailing-slash behavior relevant?
  6. Are route parameters decoded or normalized more than once?
  7. 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.orderId is a source.
  • req.user is 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

  1. Mark orderId as attacker-controlled.
  2. Locate where authentication creates req.user.
  3. Follow the controller call into the service.
  4. Confirm that identity and tenant context disappear.
  5. Inspect the repository query and find that it filters only by object ID.
  6. Inspect the response and identify exposed fields.
  7. Search for every caller of getById and findById to 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 return or 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 internalNotes field 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 amount checked 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 Host or 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; none and algorithm confusion are rejected.
  • iss, aud, exp, and nbf are validated where applicable.
  • The key is selected safely; attacker-controlled kid, jku, or x5u cannot 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.
  • state binds the authorization response to the initiating browser session.
  • OIDC nonce is 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 BY direction and field.
  • LIMIT or 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.
  • SameSite cookies 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 Origin value.
  • Access-Control-Allow-Credentials: true with an overly broad origin policy.
  • Suffix checks that allow example.com.attacker.tld.
  • Prefix checks that allow https://example.com.attacker.tld.
  • Trusting null origin 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-inline or unsafe-eval without 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-ancestors or equivalent headers.

8. Client Storage and Messaging

  • Do not store long-lived sensitive tokens in browser storage when an HttpOnly cookie design is available.
  • Validate postMessage sender 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