Est.

Root Cause Analysis After a Pen Test to Prevent Vulnerability Class Recurrence

Tracing vulnerabilities back to process gaps stops the same class from recurring annually.

· 10 min read
Cover illustration for “Root Cause Analysis After a Pen Test to Prevent Vulnerability Class Recurrence”
Security Remediation · October 10, 2026 · 10 min read · 2,219 words

If a pen test gets patched finding-by-finding, it will produce the same report again next year. Tracing each finding back to the process, training gap, or architectural decision that allowed it, and fixing that instead of the symptom, is the only way to stop a vulnerability class from recurring.

Why Patching Individual Findings Invites the Same Vulnerability Class to Return

Picture a team that finds a reflected XSS vulnerability in its search bar during a pen test. Nothing about the first fix was wrong. It never addressed why the flaw existed.

Fixing one SQL injection in one endpoint is a patch. Understanding why parameterized queries were never enforced across the entire codebase, so that every other endpoint touching a database is equally exposed, is a root cause fix. Those are different jobs, and most remediation workflows only do the first one.

This pattern occurs in organizations that run annual pen tests and treat each cycle as a standalone to-do list. It's a process that failed to catch the condition the first time and was never changed, so it fails the same way again: the code review checklist that doesn't ask about encoding, the onboarding material that never covered secure query construction, the approval workflow with no security gate.

A fair objection here: a team might fix every finding before the retest attestation and reasonably believe that's enough. Retesting validates a fix. It does not validate that the class is closed.

Root Cause Analysis in a Pen Test Report

Root cause analysis, applied to a pen test report, is the disciplined process of moving from a specific finding to the systemic driver that made it possible: a process gap, a training failure, or an architectural decision. The output is a documented answer to a structural question: what condition in the system allowed this finding to exist, and what permanent change stops the next instance of the same class.

RCA is not a blame exercise. The question on the table is why the systems meant to catch that kind of error, code review, static analysis, security training, architectural guardrails, didn't catch it or prevent it in the first place, not who wrote the vulnerable line of code. A core principle carries across every field that practices structured problem-solving: if the same class of error happens twice, the problem lives in the process, not in the person who happened to write the code.

Three techniques, well established outside of security and directly applicable to it, give that process some discipline. The 5 Whys method asks "why did this exist?" repeatedly until the chain bottoms out at something structural. Each "why" moves further from the code and closer to the organization.

The Fishbone, or Ishikawa, diagram takes a different approach, useful when a finding has more than one contributing thread. Cause mapping, a third technique, builds a visual chain connecting events and conditions, answering what happened, why it happened, and how the conditions lined up, which helps when a single finding is really the product of several weaknesses stacking on top of each other.

Reading a pen test report as an RCA input

A pen test report listing findings by severity is a starting point, not a finished assignment. The real work for an engineering leader is to regroup that list by vulnerability class, read the attack-path narrative attached to each finding, and look for the condition shared across instances that look unrelated on the surface.

Start by grouping findings by class instead of by severity or by the endpoint affected. An XSS finding in the search bar and an XSS finding in the file-upload flow look like two separate line items in a report sorted by severity. Grouped by class, they're two data points pointing at the same root cause, so treating them separately means you fix the same underlying gap twice without ever naming it once.

A report that supports real RCA work has a few things a weaker one doesn't. Attack-path context tells you exactly where a trust boundary broke: a note that a tester logged in as User A, captured a session token, and used it to pull User B's order history gives you far more than a bare CVE reference would. Exploitability proof matters too. And the report should describe the condition that made exploitation possible in the first place, whether that's a missing header, a code pattern, or a configuration default left unchanged.

A report that reads like raw scanner output, a list of CVEs with no exploitability evidence and no narrative connecting them, can't support this kind of analysis. It doesn't tell you which condition in your actual system mattered, only that a tool matched a signature. Whitebox testing, where the tester has access to source code, cloud configuration, and architecture documentation, tends to produce far richer material for this work than blackbox testing alone, because the tester can point to the specific line of code or configuration choice that created the opening.

Running the RCA session: from finding cluster to documented root cause

Diagram: From Finding to Permanent Control: The RCA Chain. Visualizes: Visualize the linear progression from a raw pen test finding to a structural SDLC control that prevents recurrence.

Once findings are grouped by class and the report has enough context to work with, the RCA session converts that cluster into a documented root cause statement naming the specific process, training, or architectural gap that produced it. The room for that session should hold the engineer who owns the affected service, the security lead or pen test liaison, and someone from the team that wrote the relevant code. This works best as a focused technical conversation among people with direct knowledge of the system, not a formal post-mortem committee.

The first step is stating the finding class as a problem statement rather than a list of tickets: "We have three instances of reflected XSS across the product," not three separate bug reports to assign out individually. The individual endpoints are symptoms of the process, training, or architectural gap that produced all three.

From there, apply the 5 Whys to the class as a whole, not to any single instance. Why does user input reach the DOM without encoding? Because the rendering function in use doesn't apply output encoding by default. Why wasn't that caught in code review? Because the review process has no security checklist, and reviewers are checking for correctness, not for encoding patterns. Why is there no security checklist? Secure coding standards were never formalized or adopted by the team. That last answer is the root cause: no secure coding standard mandating output encoding, and no review gate enforcing it.

Before you move to documentation, validate that cause against every instance in the cluster. A class of findings can have more than one root cause, and treating three instances as one problem when they're really two is its own kind of incomplete analysis.

The session closes with a documented root cause statement, structured consistently: finding class, affected systems, root cause (process, training, or architecture), contributing conditions, an owner, and both corrective and preventive actions. Corrective action closes the existing instances. Preventive action changes the system so new code can't introduce the same class going forward. Both need to be written down and verified as complete, not left as a plan someone intends to execute eventually.

What root causes look like for the vulnerability classes pen tests find most often

A predictable category of root cause explains most vulnerability classes that recur in pen test reports, and identifying that category points directly at which part of the SDLC needs the fix.

SQL injection surfaces as user input concatenated directly into a SQL query. The root cause is almost always the absence of a parameterized query standard enforced at the framework or ORM layer, often made worse by the lack of a static analysis rule that would catch string-concatenated queries before merge. The failure mode isn't even limited to naive string-building: a 2025 PostgreSQL vulnerability, CVE-2025-1094, carrying a CVSS score of 8.1, showed that SQL injection can survive even escaping routines that were assumed to be safe. The preventive target is parameterized queries enforced as a coding standard, backed by a SAST rule that flags raw string interpolation in query construction.

Cross-site scripting appears as user-controlled data rendered in the DOM without encoding, whether the variant is stored, reflected, or DOM-based. All three share the same root cause category: output encoding isn't mandated anywhere in the component library or rendering layer, and code review doesn't check for it. DOM-based XSS in particular tends to come from direct assignment to innerHTML in JavaScript written before the team adopted a framework with safer defaults. The preventive target includes a Content Security Policy header, framework-enforced output encoding, and a linting rule against raw innerHTML assignment.

Server-side request forgery surfaces when a server makes outbound requests to attacker-controlled URLs, often exposing internal services or cloud metadata endpoints in the process. The root cause is the lack of an allowlist policy for outbound request destinations, combined with cloud metadata endpoints that aren't blocked at the network or IAM layer. The 2025 OWASP Top 10 folded SSRF directly into category A01, Broken Access Control, because at its core it's a trust-boundary failure: the server reached a resource it had no business reaching. Legacy metadata endpoints on GCP's GKE are a documented real-world example, capable of exposing service account credentials to pods, since the legacy endpoint doesn't require a custom HTTP header, which turns SSRF into a credential-theft path in cloud-native environments. The preventive target is egress allowlisting, metadata endpoint blocking, and an architectural review requirement for any feature that fetches a user-supplied URL.

Insecure deserialization surfaces when an application deserializes attacker-controlled data using unsafe methods, such as Java's ObjectInputStream.readObject() or Python's pickle.loads() applied directly to request data. The fix for this class replaces unsafe deserialization with data-only formats like JSON paired with schema validation, and where binary deserialization genuinely can't be avoided, adds allowlist-based input filtering ahead of the deserialization call itself.

Hardcoded secrets occur when cryptographic keys, API tokens, or database passwords are committed straight into source code, the kind of line that reads like private static final String DB_PASSWORD = "prod_secret_2026". The preventive target is a centralized secrets vault, HashiCorp Vault being one widely used example, along with pre-commit secret scanning and a repository scan for credentials already leaked into history.

Translating root causes into permanent SDLC controls that close the class for future code

A root cause statement only does its job once it becomes a control embedded in the SDLC, something that applies automatically to every new line of code rather than depending on someone remembering the lesson.

Three categories of control map to the root causes above. Process controls include secure coding standards, security-specific items added to code review checklists, and pull-request templates that require a security sign-off for any feature touching authentication, data rendering, or outbound requests. Tooling controls are SAST rules wired into the CI pipeline that fail a build outright when it detects raw SQL string concatenation, direct innerHTML assignment, or credential-named string literals, along with dependency scanning for known-vulnerable library versions. Architectural controls enforce safe defaults at the framework layer, parameterized query APIs and output-encoding rendering functions that make the dangerous pattern harder to reach than the safe one, plus network egress policies that block unintended outbound requests at the infrastructure level before they ever become an SSRF finding.

ISO 27001's Annex A 8.29 requires that security testing be defined and implemented across the development life cycle, with implementation guidance recommending coverage at the unit, integration, system, and acceptance phases, using static analysis, dynamic analysis, fuzzing, dependency scanning, and penetration testing together. Embedding security controls into the SDLC, under that standard, is a compliance requirement as much as it is good engineering practice.

The test for whether a preventive control is real comes down to one question: if a developer introduced the same root-cause condition tomorrow, would the code be stopped before it merges? If the only thing standing between that condition and production is a human reviewer noticing the pattern, the control is weak. If a pipeline gate catches it automatically, the control is structural. Pull-request scanning, in that sense, is the natural complement to periodic pen testing: it catches a vulnerability class the moment it reappears in new code, rather than letting it accumulate quietly until the next annual test turns it into a finding cluster all over again.

Good training remediation when the root cause is knowledge, not tooling

Not every root cause resolves with a pipeline gate or a framework default. When the 5 Whys chain ends at "developers were never trained on why string concatenation in SQL queries is dangerous," the preventive action has to be training, and it has to be built with the same rigor as a tooling control, not treated as a slide deck shown once during onboarding.

Effective training remediation targets the specific vulnerability class the RCA session identified, using real examples from the organization's own codebase. You reinforce it at the point where a developer would actually apply it, inside code review, inside the pull-request template, inside the same CI feedback loop that flags the pattern automatically. And it gets measured the same way a tooling control gets measured: does the class recur in code written after the training was delivered? If it does, the training wasn't the root cause fix. Something upstream of it still needs naming.

More in Security Remediation