Guide
The vulnerability remediation process, step by step
BaseCobalt · Updated
Remediating a software vulnerability follows the same sequence however it was found: confirm the finding applies to current code, understand the affected code, reproduce the issue, assess whether it is exploitable, patch the root cause, validate that nothing else broke, and verify that the original vulnerability is gone. Skipping reproduction or verification is how fixes end up incomplete.
1. Intake: normalize the finding
Findings arrive in different shapes. A SAST scanner emits SARIF with a rule ID, a file and line, and sometimes a source-to-sink trace. A dependency scanner reports a package, a version range and an advisory (a CVE or GHSA ID). A penetration test or bug bounty report is prose with reproduction steps against a specific environment.
Before any investigation, extract the same facts from each:
- The location: repository, file, line, and the commit the scan ran against.
- The vulnerability class, ideally as a CWE.
- The evidence the reporter had: a data-flow trace, a request, an advisory.
Then check the finding still applies. Code moves between the scan and the fix; a finding reported against last month’s commit may point at lines that no longer exist, or may already be fixed.
2. Investigate the affected code
Investigation answers how the flagged code is actually used. For injection-style findings that means tracing data flow from untrusted input (a request parameter, a header, a file, a message) to the dangerous operation (a query, a shell call, a template, a deserializer). Along the way, note:
- Validation or sanitization the scanner may not have modeled.
- Framework protections: parameterized ORM queries, template autoescaping, CSRF middleware.
- Every caller of the vulnerable function, since the fix may need to cover more than one path.
- Whether the code is reachable from an exposed entry point at all.
For configuration findings (Dockerfiles, Kubernetes manifests, Terraform) investigation also covers what depends on the current behavior, such as files the process writes or ports it binds.
3. Reproduce the vulnerability
A reproduction is a concrete check that demonstrates the vulnerable behavior against the current source, in an isolated environment. It can be a failing unit or integration test, a request sent to a local instance, or, for configuration findings, an inspection of the built artifact, such as confirming which user a container image runs as.
Reproduction does two jobs. It separates true positives from false positives with evidence rather than argument. And it gives the fix a target: a check that must go from failing to passing. Reproductions should never run against production systems.
4. What is exploitability assessment?
Exploitability assessment determines whether an attacker can actually trigger a vulnerability in this codebase and deployment, and what they would gain. The questions are specific:
- Does attacker-controlled input reach the vulnerable code?
- Is authentication or a particular role required first?
- What preconditions must hold: a feature flag, a configuration, a race?
- What is the impact: data read, data modified, code executed, service denied?
- Do compensating controls already block the path?
This is different from a CVSS base score, which describes a vulnerability in general, and from EPSS, which estimates how likely a published CVE is to be exploited in the wild. Neither says whether the vulnerable path is reachable in your code. Exploitability sets priority; it doesn’t decide whether a finding is real. A flaw that is unreachable today can become reachable with the next feature, so cheap fixes to unreachable code are often still worth making.
5. Patch the root cause
A good security patch:
- Fixes the flaw at the right layer: a parameterized query rather than string escaping, an allowlist rather than a denylist.
- Covers every path found during investigation, not only the line the scanner reported.
- Is minimal and uses the codebase’s existing helpers and conventions.
- Changes no behavior beyond what the fix requires.
- Adds a regression test built from the reproduction, where the project has tests.
Patches to avoid: suppressing the rule, rewriting code so the scanner’s pattern no longer matches while the exploit path remains, catching and swallowing errors, or disabling the feature.
6. Validate that nothing else broke
Run the project’s own checks (tests, build, type check, lint) before applying the patch and again after. The baseline run matters: if the suite was already failing, a failure after the patch proves nothing, and a pass after the patch means less. The result to look for is PASS before and PASS after.
7. How do you verify a vulnerability fix?
Verification shows the vulnerability is gone; validation only shows nothing else broke. To verify a fix:
- Re-run the reproduction and confirm the vulnerable behavior no longer occurs.
- Rescan the patched code with the same rule and confirm the original finding no longer fires.
- Confirm the rescan reports no new findings introduced by the patch.
- Check sibling code paths for the same flaw.
What makes a patch safe to merge?
A security patch is ready for review when the reviewer can see:
- A reproduction that failed before the patch and passes after it.
- Baseline and post-patch validation results, both passing.
- A clean rescan: original finding resolved, nothing new introduced.
- A small diff with an explanation of the root cause it addresses.
With that evidence, review means checking the work rather than redoing the investigation. Merging stays a human decision.
Worked example: a container that runs as root
This is a shortened BaseCobalt run on a CWE-250 finding at Dockerfile:14 in BaseCobalt’s vulnerable-app fixture. Each step above appears in the output:
$ basecobalt fix ct-f663e3
✓ Finding reproduced current source
✓ Repository investigated 3 files
✓ Baseline validation passed test · build
✓ Patch applied +2 −0
✓ Validation passed PASS → PASS
✓ Security rescan passed 0 new findingsThe patch was two lines:
EXPOSE 3000
+ RUN chown -R node:node /app/uploads
+ USER node
CMD ["node", "server/index.js"]The USER node line removes root. The chown line keeps the application able to write to its uploads directory once it no longer runs as root, which is what the baseline and post-patch validation guard. The full run has the details.