Audit The Security Boundary
Code auditing starts by finding the security boundaries. Authentication, authorization, object ownership, file access, network requests, command construction, database queries, template rendering, deserialization, and plugin loading all define places where trust changes. A useful audit does not read every line with the same intensity. It prioritizes paths where attacker-controlled input reaches privileged behavior.
I first map roles, entry points, and dangerous operations. Then I trace how data moves between them. This turns the audit from a search for patterns into a review of assumptions. The real question is not whether a function looks unsafe. The question is whether an attacker can reach it with meaningful control.
Follow Input Across Time
Many bugs are not immediate. Input can be stored, normalized, queued, cached, exported, rendered later, or processed by a background worker. A form that only stores data may still lead to stored XSS, template injection, SSRF, file write, or command execution when another component consumes that data.
A good audit follows input across time. I track where the value is created, where it is stored, where it is transformed, where it is trusted, and where it reaches a sink. Delayed sinks are especially important in content management systems, plugins, importers, reporting features, webhook processors, and administrative panels.
Compare Similar Paths
One of the strongest audit methods is comparing similar code paths. If the normal edit route checks ownership but the AJAX edit route does not, that difference is a signal. If the preview route escapes output but the export route does not, that difference is a signal. If the API checks role permissions but the background task trusts a queued object, that difference is a signal.
Security bugs often appear where developers duplicated logic or built a secondary path for convenience. Reviewing differences between create, read, update, delete, export, import, preview, and background execution paths can reveal issues that simple grep-based auditing misses.
Prioritize Useful Sinks
Not every sink is equally important. I prioritize sinks that can cross a boundary: SQL builders, file operations, command execution, SSRF-capable network clients, XML parsers, archive extraction, template engines, dynamic includes, serialized object loading, permission checks, and administrative rendering.
When a sink is found, the next step is reachability. Which role can reach it? What input is controlled? What validation happens? Can validation be bypassed through encoding, path normalization, alternate content type, stored state, or request ordering? Those questions decide whether the sink is a vulnerability or only an implementation detail.
Turn Findings Into Fixable Reports
A code audit is successful when the result is actionable. The final note should identify the broken assumption, affected path, attacker-controlled input, security impact, and fix direction. A maintainer should be able to understand why the issue exists without reading the entire codebase.
The best fixes usually move validation and authorization close to the sensitive operation. Helper functions are useful, but they must be used consistently. The report should explain where the control belongs, not only where the payload worked.