November 5, 2026
Secure Code Review Explained: How VAPT Teams Do It, Manual and Automated
How security teams read a codebase for vulnerabilities: the manual passes, the automated tools like Semgrep and CodeQL, and how the two…
By Vigneshkumaroffsec
8 min read
How security teams read a codebase for vulnerabilities: the manual passes, the automated tools like Semgrep and CodeQL, and how the two work together end to end.
By Vignesh Kumar | Security Researcher
A penetration test looks at the application from the outside. It sends requests and watches what comes back.
A secure code review looks from the inside. It reads the source and asks a different question: not "what can I reach from here," but "where does untrusted data go, and what happens when it arrives."
The two find different bugs. A scanner sees a login form. A code reviewer sees that the login handler builds its SQL query with string concatenation, and that the "reset password" token is generated with a predictable value. Some of the most serious vulnerabilities, like a missing authorization check deep in a service, are nearly invisible from the outside but obvious in the code.
This article explains how VAPT teams actually do a source code review: the manual methodology, the automated tooling, and how they combine. It is written for people reviewing code they own or are authorized to assess.
Why code review finds what pentesting misses
Both approaches matter, and good teams do both. But reading the code has specific advantages:
- Coverage. A pentest only tests the paths it can discover. A review can look at every endpoint, including the ones nothing links to.
- Root cause. From the outside you see a symptom. In the code you see the exact line, which makes the fix precise.
- Logic and authorization. Missing access-control checks and broken business logic often don't show from outside at all. In the code, they are a missing
if. - Secrets and crypto. Hardcoded keys, weak hashing, and bad randomness are visible immediately in source, and hard to spot over the wire.
The trade-off is that code review needs access to the source, and it needs time. That is why teams lean on automation for breadth and keep humans for depth.
The methodology VAPT teams follow
A review is not "open the repo and start reading top to bottom." It is a structured process. Most teams run roughly these five stages.
Stage 1: Understand the application first
You cannot spot a dangerous pattern in code you don't understand. So the first pass is orientation, not bug hunting.
- What does the app do, and what is the sensitive part? Payments, personal data, admin functions. That is where the effort goes.
- What is the stack? Language, framework, ORM, and major libraries decide which bugs are even possible and which the framework already prevents.
- How is it structured? Find the routes or controllers, the authentication layer, the database access layer, and the configuration.
A few commands get you oriented fast:
# What languages and how big
cloc .
# Find the dependency manifests
find . -name 'package.json' -o -name 'pom.xml' -o -name 'requirements.txt' -o -name 'go.mod'
# Find where routes are defined (framework-dependent)
grep -rniE '@(Get|Post|Put|Delete)Mapping|app\.(get|post)|router\.' --include=*.{java,js,ts,py,go} . | head# What languages and how big
cloc .
# Find the dependency manifests
find . -name 'package.json' -o -name 'pom.xml' -o -name 'requirements.txt' -o -name 'go.mod'
# Find where routes are defined (framework-dependent)
grep -rniE '@(Get|Post|Put|Delete)Mapping|app\.(get|post)|router\.' --include=*.{java,js,ts,py,go} . | headStage 2: Map the entry points
Every vulnerability starts with input the attacker controls. So the next step is to list where untrusted data enters the application:
- HTTP request handlers: parameters, body, headers, cookies.
- File uploads.
- API endpoints and webhooks.
- Message queues and background jobs that process external data.
- Anything that deserializes input.
These entry points are your sources, the places tainted data comes from. Everything dangerous happens between a source and a sink.
Stage 3: Trace source to sink
This is the core technique of manual code review, and it is worth understanding well.
A source is where untrusted input enters. A sink is a sensitive operation where that input can cause harm:
- A database query → SQL injection.
- A shell command → command injection.
- An HTML response → cross-site scripting.
- A file path → path traversal.
- An HTTP request built from input → server-side request forgery.
The reviewer follows the data from a source toward a sink and asks one question the whole way: is the input validated, sanitized, or safely handled before it reaches the sink? If yes, it's fine. If it reaches the sink unchanged, you have a finding.
Here is the pattern in its simplest form:
# SOURCE: user controls the "name" query parameter
name = request.args.get("name")
# ... passes through the code unchanged ...
# SINK: used to build a SQL query by string formatting
cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")# SOURCE: user controls the "name" query parameter
name = request.args.get("name")
# ... passes through the code unchanged ...
# SINK: used to build a SQL query by string formatting
cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")Input goes from a source to a SQL sink with no parameterization in between. That is SQL injection, visible in two lines once you follow the data.
The same shape covers command injection (os.system(user_input)), XSS (response.write(user_input)), and path traversal (open(base + user_input)). Learn to see the source-to-sink flow and most injection bugs look the same.
Stage 4: Review the security-critical areas by hand
Beyond injection, experienced reviewers go straight to the areas that are historically where the serious bugs live. These map closely to the OWASP Top 10:
- Authentication. How are passwords stored? Is it a slow hash like bcrypt or Argon2, or a plain MD5? Are reset tokens random and single-use? Is there rate limiting?
- Authorization. This is the big one. For every sensitive action, is there a check that this user is allowed to act on this object? A missing check is broken access control, the top item on the OWASP list, and it rarely shows from outside.
- Session management. Secure, HttpOnly cookies? Sessions invalidated on logout and password change?
- Secrets. Any hardcoded API keys, passwords, or private keys in the repo or its history?
- Cryptography. Strong algorithms, random IVs, no home-made crypto, secure randomness for anything security-sensitive.
- Deserialization and SSRF. Untrusted input into a deserializer or into an outbound request.
- Dependencies. Known-vulnerable libraries, which automation handles well.
Stage 5: Verify, rate, and report
Every candidate finding is confirmed by reading the surrounding code, then rated by severity (usually CVSS) and written up with the exact file and line, an explanation of the impact, and a concrete fix.
We'll come back to the report, because it's what the client actually pays for.
The automated half: tools VAPT teams run
No team reads a million lines by hand. Automation covers breadth, so humans can spend their time on depth. These are the categories and the tools that show up most often.
SAST: static analysis
Static Application Security Testing tools read the code without running it and flag dangerous patterns. Two open-source tools dominate.
Semgrep matches patterns that look like code, which makes custom rules easy to write. It is fast and good in CI:
# Run a broad security ruleset
semgrep --config=auto .
# Or a specific registry ruleset
semgrep --config=p/owasp-top-ten .# Run a broad security ruleset
semgrep --config=auto .
# Or a specific registry ruleset
semgrep --config=p/owasp-top-ten .A Semgrep rule for the SQL pattern above is readable on its own:
rules:
- id: python-sql-string-concat
languages: [python]
message: SQL query built from a string, possible SQL injection
severity: ERROR
patterns:
- pattern: $CURSOR.execute("..." + $X)rules:
- id: python-sql-string-concat
languages: [python]
message: SQL query built from a string, possible SQL injection
severity: ERROR
patterns:
- pattern: $CURSOR.execute("..." + $X)CodeQL, from GitHub, treats the codebase as a database you query. It does deeper data-flow (taint) analysis, so it can actually follow a source all the way to a sink across functions, which is exactly the manual technique, automated:
codeql database create db --language=javascript
codeql database analyze db --format=sarif-latest --output=results.sarifcodeql database create db --language=javascript
codeql database analyze db --format=sarif-latest --output=results.sarifOther tools you'll see: Bandit (Python), gosec (Go), Brakeman (Ruby on Rails), and commercial platforms like Checkmarx, Fortify, and Sncode.
SCA: dependency scanning
Most of the code in a modern app is third-party. Software Composition Analysis checks your dependencies against known-vulnerability databases:
# Trivy scans the filesystem for vulnerable dependencies
trivy fs .
# OWASP Dependency-Check and osv-scanner do the same
osv-scanner -r .# Trivy scans the filesystem for vulnerable dependencies
trivy fs .
# OWASP Dependency-Check and osv-scanner do the same
osv-scanner -r .Trivy, OWASP Dependency-Check, osv-scanner, and GitHub's Dependabot all do this. It is low effort and catches real, exploitable issues.
Secret scanning
Hardcoded secrets are one of the most common and most serious findings. Scanners check both the current code and the git history, because a key committed once and "removed" later is still in the history:
# Scan the full git history for secrets
gitleaks detect --source . -v
# trufflehog does the same and can verify if a key is live
trufflehog git file://.# Scan the full git history for secrets
gitleaks detect --source . -v
# trufflehog does the same and can verify if a key is live
trufflehog git file://.Gitleaks and TruffleHog are the standard choices.
Where automation falls short
Automated tools are essential, but every VAPT team knows their limits:
- False positives. Tools flag patterns that are safe in context. Someone has to triage.
- False negatives. They miss what they have no rule for.
- Logic and authorization. A tool does not know that this endpoint should be admin-only. Business logic and broken access control are largely invisible to SAST.
That last point is why the human stage is not optional. The tools find the needle-in-a-haystack injection bugs; people find the missing check that no rule describes.
How manual and automated fit together
The real workflow in a VAPT engagement is not "manual or automated." It is a loop where each feeds the other:
- Automate first for breadth. Run SAST, SCA and secret scanning across the whole codebase to get a map of the obvious issues and the risky areas.
- Triage the output by hand. Confirm the true positives, discard the noise. This alone needs a human who understands the code.
- Review the critical areas manually. Authentication, authorization, and business logic, the things tools can't judge. Use the automated findings to point you at the risky files.
- Write custom rules from what you find. Found a dangerous internal function used unsafely? Write a Semgrep rule for it, so every other use in the codebase is found automatically, and every future one is caught in CI.
- Verify dynamically where you can. Confirm a suspected bug against a running instance you're authorized to test. Code review and DAST confirm each other.
That fourth step is the mark of a mature team: the manual review makes the automation smarter, and the automation makes the next review faster.
Shifting it left: code review in CI/CD
The same tools run automatically on every change, so issues are caught in the pull request instead of a once-a-year audit:
# A minimal security gate in GitHub Actions
- name: Semgrep
run: semgrep ci --config=auto
- name: Dependency scan
run: trivy fs --severity HIGH,CRITICAL --exit-code 1 .
- name: Secret scan
run: gitleaks detect --source . --redact# A minimal security gate in GitHub Actions
- name: Semgrep
run: semgrep ci --config=auto
- name: Dependency scan
run: trivy fs --severity HIGH,CRITICAL --exit-code 1 .
- name: Secret scan
run: gitleaks detect --source . --redactTwo things keep this from becoming noise that developers ignore:
- Tune the rules. Start with high-confidence rules only, so the signal stays high. Add more as the team learns to trust it.
- Fail the build only on what matters. Block on high-severity and confirmed secrets; report the rest without blocking.
Automated gates don't replace a periodic deep manual review. They catch the routine issues continuously, so the human review can spend its time on logic and design.
What the deliverable looks like
A VAPT code review is sold on its report, not its tool output. A raw SAST export is not a deliverable. A good finding contains:
- Title and severity (CVSS score and vector).
- Location: exact file and line.
- Description: what the flaw is and why it matters here, in plain language.
- Proof: the vulnerable code, and a demonstration of impact where safe to show.
- Remediation: the specific fix, ideally with a corrected code snippet.
- References: the matching OWASP / CWE identifier.
Findings are ordered by risk, with an executive summary on top for the people who decide what gets fixed. The value a team adds is turning tool noise and manual insight into a short list of real, prioritized, fixable problems.
Common Mistakes to Avoid
- Running a SAST tool and shipping its raw output as the report.
- Reviewing code top to bottom instead of following the data.
- Skipping authorization review because tools don't flag it.
- Ignoring the git history when scanning for secrets.
- Chasing every low-severity lint finding while missing one broken access-control check.
- Not writing custom rules for bugs you've already found once.
- Reviewing the code but never confirming findings against a running build.
A Quick Secure Code Review Checklist
- Application understood: purpose, stack, structure, sensitive areas.
- Entry points (sources) mapped.
- Source-to-sink traced for injection, XSS, SSRF, path traversal.
- Authentication: strong password hashing, safe reset tokens, rate limiting.
- Authorization checked on every sensitive action and object.
- Session handling and cookie flags reviewed.
- Secrets scanned in code and full git history.
- Cryptography uses strong, standard algorithms and secure randomness.
- Dependencies scanned for known vulnerabilities.
- SAST run, output triaged by a human.
- Custom rules written for new patterns found.
- Findings rated, prioritized, and written up with fixes.
Final Thoughts
Automated tools and manual review are not competitors. The tools give you breadth across a codebase no person could read in full. The human gives you the judgment the tools don't have, especially for authorization and logic, which is where the serious bugs usually hide.
The best reviewers use each to make the other better: automation points them at the risky code, and what they find by hand becomes the next rule the automation runs forever.
If you want to start, take one application you own, run Semgrep and Gitleaks over it, and then trace a single user input from where it enters to where it's used. That one trace teaches the whole method better than any checklist.
About me
I'm Vignesh Kumar, a security researcher working across application security, DevSecOps, cloud security, and vulnerability research.
I share practical findings from security assessments and responsible disclosures to help improve the security of real-world systems.
The techniques in this article are intended for code you own or are explicitly authorized to review.