> For the complete documentation index, see [llms.txt](https://docs.nullify.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nullify.ai/capabilities/pentests/exploit-validation.md).

# Exploit Validation

> **Maturity: GA** — Runs from policies you own, stored per tenant (`dast_exploit_validation_policies` in `hyperdrive/schemas/full_schema.sql`).

## Overview

Exploit Validation proves whether an individual finding is *actually* exploitable by attacking a running target — without launching a full [Pentest](/capabilities/pentests.md). When code review flags an injection point, or dependency analysis surfaces an exploitable CVE, Nullify can drive a focused exploitation agent against the deployed application, read the vulnerable source, reach the code path over real HTTP, and record the result as evidence.

Where a Pentest goes *broad* — mapping an application and hunting for anything exploitable — Exploit Validation goes *narrow*: one already-known finding, one verdict, backed by proof. It turns a theoretical "this looks reachable" into a confirmed "this was reproduced" (or a defensible "this could not be exploited").

Validation runs against the Web Services and Targets you register in your inventory, and it only runs when a **policy** you control says it should.

{% hint style="info" %}
Exploit Validation reuses the same evidence-first exploitation engine as Pentests, scoped to a single finding. A validated finding carries the same reproduction evidence you would get from a full engagement.
{% endhint %}

## How it works

1. **A finding becomes a candidate.** During [Triage](/capabilities/triage.md), a code-review (SAST) or dependency (SCA) finding is assessed as reachable and likely exploitable.
2. **Policies decide whether to run.** Nullify checks your Exploit Validation [policies](#policies). A policy must be both enabled and *armed* before it can start anything. If an armed policy matches the finding — by finding source and validation window — and the finding isn't inside its revalidation cooldown, validation is started against a single non-production target.
3. **A target is resolved.** The finding's repository is matched to the Web Services named in the policy. Nullify attacks exactly one target per run, and only a target you have **confirmed as non-production** — a target left unclassified is refused just like a production one, with a distinct reason recorded. Nullify records which target it was permitted to attack.
4. **The agent attempts the exploit**, within a fixed boundary (see below). A focused agent reads the vulnerable code, discovers the reaching endpoint, and sends bounded HTTP requests, using out-of-band callbacks where blind exploitation requires it. Requests are read-only unless you have accepted writes for that target (see [What validation is allowed to do](#what-validation-is-allowed-to-do)). Credentials are available only under an explicitly granted authenticated-session capability; anonymous validation does not inherit them. Captured credential material is subject to redaction.
5. **An outcome is recorded.** The agent returns a verdict with proof. The result is written back onto the finding's triage, marks a confirmed finding as **Exploit validated**, and appears as an **attempt** on the Exploit Validation page. It does not yet change the finding's exploitability, priority or severity (see [How validation feeds triage](#how-validation-feeds-triage)).

The **Exploit Validation** page holds both halves: the policies that govern validation, and the attempts that have run.

## Outcomes

Every validation run is recorded as an **attempt**. While a run is underway the attempt shows as **In progress**; when it finishes it lands on one of these outcomes:

| Outcome              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Confirmed**        | Nullify proved exploitability with a concrete HTTP response or an out-of-band callback. The finding is exploitable.                                                                                                                                                                                                                                                                                                                                                                   |
| **Not Exploitable**  | The correct reachable code path was exercised with the context it needed and the exploit still did not work — a strong negative signal. Reported only when the run actually sent requests.                                                                                                                                                                                                                                                                                            |
| **Cannot Determine** | No verdict was reached. Either validation was blocked (the target was unreachable, access was blocked, authentication was required, a rate limit or request budget was hit, or context was missing), the vulnerability could not be reached within what validation is allowed to send (see [What validation is allowed to do](#what-validation-is-allowed-to-do)), or Nullify declined to carry the agent's result forward because it could not be matched to the traffic of the run. |

A **Cannot Determine** result is never a pass or a fail on your application. The recorded reason tells you which of the three it is:

* **The run was blocked.** Unreachable target, blocked access, auth required, budget exhausted, and so on. The test setup needs attention, and a re-run can then reach a real verdict.
* **Out of scope.** The vulnerability is only reachable with a request validation is not permitted to send on this target, such as a form submission or another state-changing request on a target without write acceptance. Nullify does not attempt it, and deliberately does *not* report it as Not Exploitable, because a request that was never made is not evidence of anything. The finding keeps whatever priority triage gave it.
* **Downgraded by Nullify.** The agent reported a result its own traffic does not support: it claimed an exploit without naming a request the run actually made or a callback that actually arrived, or it reported the finding not exploitable without the run having sent a single request. The report is kept, with a note saying so, and the claim is not carried onto the finding. This is deliberately strict in both directions — an unproven Confirmed moves your team onto a vulnerability nobody has shown to be real, and an unearned Not Exploitable pushes a real one down the queue.

### Declined

An attempt shows as **Declined** when validation could not log in to your application, so it never tested the finding. The reason says which:

* **Credential login failed.** A credential configured for the target is missing, or your application rejected it. Fix the credential in the target's configuration.
* **Login agent failed.** Nullify's login step broke before it could tell whether your credentials work. This is on Nullify's side; nothing needs changing in your configuration.

A declined finding is held for 24 hours before it is validated again, because every retry is a real login attempt against your application. The finding keeps its previous verdict, shown beside the decline. Once the cause is fixed, Nullify can release the hold so the finding is validated straight away; the attempt then shows as **Released by operator** until the next validation runs.

The attempts table lists each attempt's finding, source, outcome, confidence, any exploited CVE, and when it was validated — newest first.

## What validation is allowed to do

Validation sends real traffic at a real application, so what it may send is fixed rather than left to the agent:

* **Read-scoped by default.** The baseline grants `anonymous_read` and limits exploit requests to allowed read methods and inputs. Granting `authenticated_session`, where enabled, adds the narrowly scoped login/session setup needed for authorized credentials; it does not grant general mutation testing. A nominally read-only request can still trigger poorly designed application behavior, so confirmed non-production targets remain required.
* **Writes only where you have accepted them.** On a target you have confirmed as non-production and for which you have accepted write risk, validation may send the `POST`, `PUT`, `PATCH` or `DELETE` request a finding's exploit needs. It prefers the least-destructive proof, registers each write before sending it, and the verdict lists every write it sent, including one that may have been committed without a response. Acceptance is per target and you record it yourself, or Nullify staff record it on your behalf; see [Accepting write risk on a target](#accepting-write-risk-on-a-target). It is cleared if the target's host, port or protocol changes or the target stops being confirmed non-production. A production target is never attacked, with or without acceptance.
* **One target.** Only the host and port of the target the policy authorised. A request to any other host or port, including one reached by following a redirect, is refused before it is sent.
* **Refused before the request leaves.** Both limits are enforced at the network boundary, not by asking the agent to behave. A request outside them never reaches your application.

The trade is that, on a target without write acceptance, a vulnerability only reachable through a state-changing request cannot be proven; it comes back as **Cannot Determine**, out of scope, rather than as a false negative.

## Accepting write risk on a target

Write acceptance is a decision about one target, made by a person. Without it, validation on that target stays read-only.

**Who can accept.** An administrator on your side (today, the admin role). Accepting records who accepted, when, and which version of the warning they read. Service accounts and API tokens cannot accept, because the record has to name a person. Revoking needs only the configuration write permission.

**If your administrator approves outside the dashboard.** If you tell your Nullify contact in writing (for example on Slack) that write risk is accepted for a target, a verified member of Nullify staff can record the acceptance for you. They must name the person at your company who approved and link the message or ticket that shows it. The panel then reads "Accepted (time) by (staff member) (Nullify staff) on behalf of (your contact)" with the evidence shown as a link to its host, and anyone who can open the target sees it. Nullify staff can only do this for a tenant they have been granted access to, and Nullify does not verify the link or the contact: it is Nullify's attestation of what you told it. Your own administrator can revoke it at any time.

**Before you accept.** The target must be [confirmed as non-production](#how-it-works). A production or unclassified target cannot be accepted.

**In the dashboard.** Open **Applications → Targets**, edit the target, and find **Exploit validation writes** under the classification. Select **Review and accept write risk**, read the warning, confirm the endpoint shown is the one you mean, tick the acknowledgment and select **Accept write risk**. The acceptance is bound to the saved endpoint: if you have unsaved host, port or protocol edits in the form, save them first.

**What the warning says.** In summary: validation may send state-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`) to this target, including ones that create, modify or delete records, trigger downstream workflows, and payloads that are destructive or time-based. These are real writes that Nullify records before sending and lists in the verdict, but does not reverse. The acceptance covers this target only, while it stays confirmed non-production at the same host, port and protocol. Accept it only for a staging or test environment whose data you are willing to have modified. The full text is shown in the dialog and returned by the API.

**When it takes effect.** Validation runs authorized after the acceptance is recorded can send writes. A run that was already authorized stays read-only.

**Revoking.** Select **Revoke for new runs** on the same panel. Runs that have not started send no writes. A validation that is already running is not stopped and keeps the writes it started with, and revoking does not undo changes already made.

**Updated warning.** If Nullify updates the warning text, earlier acceptances stop applying (the panel shows **Warning outdated**) until someone accepts the new text.

**API.** `GET`, `PUT` and `DELETE` on `/dast/targets/{targetId}/exploit-validation-consent` read, accept and revoke the acceptance; `PUT` takes the warning version and the endpoint you reviewed. Accepting records who accepted, so it is done in the dashboard or through the API as a signed-in person with the arming permission; revoking needs only the configuration write permission. The read response says who accepted and, for an acceptance recorded by Nullify staff, the customer contact and the evidence link.

## Evidence

Every validated finding carries its exploit-validation report inline, on the finding itself: the outcome, Nullify's confidence, how long the run took, the endpoints it tested, a runnable **reproduction script**, and a written report covering the vulnerability, how it was exploited, the impact, and the fix — followed by the proof-of-exploit requests themselves.

Because the reproduction script is a plain shell script against your own environment, you can re-run Nullify's proof yourself — and re-run it after a fix to confirm the exploit no longer works.

## Policies

Policies are how you control **whether, when, and against what** Exploit Validation runs. Validation attempts are live LLM-driven exploitation runs, so nothing fires until a policy you have configured matches a finding. A finding is validated only when at least one **enabled and armed** policy matches it and its authorization is still valid.

Each policy defines:

| Setting                   | What it controls                                                                                                                                                                                                                                                     |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**                  | A label for the policy.                                                                                                                                                                                                                                              |
| **Policy enabled**        | Turns the policy on or off. Disabled policies never start exploit validation.                                                                                                                                                                                        |
| **Policy armed**          | Separate from enabled, and required. Enabled means the policy is configured; armed means validation may send real traffic at your target. A policy is created unarmed, and arming is a deliberate step rather than a setting you can flip in the normal policy form. |
| **Finding sources**       | Which sources the policy applies to — **SAST** (source-code vulnerabilities), **SCA Dependencies** (vulnerable open-source dependencies), or both. At least one is required.                                                                                         |
| **Web services**          | Which registered Web Services validation may run against. At least one is required — this is how a finding's repository is mapped to a live target to attack. Pick existing services from your inventory, or create one inline.                                      |
| **Validation window**     | The days and local-time window in which new validations may *begin* — for example weekdays 09:00–17:00 in `Australia/Sydney`, or "all day" on the days you select.                                                                                                   |
| **Revalidation cooldown** | How many days before the same finding can be validated again (default 30, minimum 7). Prevents the same finding from being re-attacked on every scan.                                                                                                                |

![Current validation-policy dialog](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-16dbfa16b6fb78e5fa21e9ab0bf4dd09638383c3%2Fvalidation-policy.png?alt=media)

This sample shows the required web-service setup before the policy can be created. Policy creation and arming remain separate operations.

Two details worth knowing about the validation window: it gates when a validation may **start**, so a run already underway is allowed to finish rather than being cut off at the boundary; and an overnight window (one whose end time is earlier than its start) belongs to the day it *starts* on.

Findings are also held to a severity and exploitability floor, so low-severity or unlikely-to-be-exploitable findings aren't attacked. By default a finding must be **High** severity or above and assessed as **Likely** exploitable by [Triage](/capabilities/triage.md); where the controls are available you can tune both floors per policy.

When a finding is proposed for validation, Nullify keeps only the policies that are enabled, armed, and match on finding source, validation window, and those floors. If any match, it then checks the finding's most recent attempt against the cooldown, and resolves the matching web services to targets. Exactly one target is attacked, and only one you have confirmed as non-production. If nothing matches — no armed policy applies, the finding is inside its cooldown, the finding is no longer live (fixed, dismissed, superseded), no matching policy has a web service for the finding's repository, or the only targets are production or unclassified — validation is skipped and the reason is recorded (a production and an unclassified target are refused under separate reasons).

## Authorization flow

```mermaid
flowchart TD
    Finding["Eligible SAST or SCA finding"] --> Policy{"Matching policy<br/>enabled and armed?"}
    Policy -->|No| Skip["Skip and record reason"]
    Policy -->|Yes| Target{"One authorized target<br/>confirmed non-production?"}
    Target -->|No| Skip
    Target -->|Yes| Consent{"Current consent<br/>and supported capabilities?"}
    Consent -->|No| Skip
    Consent -->|Yes| Boundary["Enforce destination and request scope<br/>before target traffic"]
    Boundary --> Evidence["Capture evidence and record outcome"]
```

## How validation feeds triage

The validation result is stored on the finding's [Triage](/capabilities/triage.md) record with its proof. A **Confirmed** outcome marks the finding **Exploit validated**, which you can filter findings on. A verdict does not yet change the finding's exploitability, priority or severity, in either direction: a **Confirmed** finding keeps the ranking triage gave it, and so does a **Not Exploitable** one.

## Where to find it

In the current dashboard, open **Settings → Exploit validation** when enabled for your tenant. Older dashboards expose the page under **Assessments**. From there you can:

* Create, edit, enable/disable, and delete **validation policies**.
* Review every validation **attempt**, newest first, with its outcome and confidence.
* Jump from an attempt straight to the finding it validated.

Each validated finding also shows its full exploit-validation report inline, in the finding's own detail view.

## Running validation via the API

Exploit Validation is available through the [Nullify API](/api-reference/api-reference.md) under the `/dast/exploit-validation` namespace — list attempts, manage policies, and request validation for a specific finding.

## Exploit Validation vs Pentests

| Aspect       | Exploit Validation                                   | Pentests                                                            |
| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------- |
| **Scope**    | One known finding (from code review or dependencies) | A whole application                                                 |
| **Goal**     | Prove or disprove a single finding's exploitability  | Discover and exploit anything reachable                             |
| **Trigger**  | Policy-driven, off the back of triage                | On-demand                                                           |
| **Depth**    | A focused attack on one code path                    | Endpoint mapping, fuzzing, and multi-stage exploit chains           |
| **Evidence** | Reproduction proof for one finding                   | Request/response pairs and reproduced exploit chains across the app |

## Arming and consent

Arming is a separate API operation with a dedicated permission, consent revision, and safety-contract version. The default capability is `anonymous_read`. `authenticated_session` requires safety contract version `2` and must also be enabled for your deployment. Unsupported capabilities are rejected.

Changes that invalidate the authorized scope require fresh authorization. Recheck the policy's armed state after changing its services or targets, and explicitly re-arm only after reviewing the new scope. An ordinary policy update is not consent to attack a different target. See the policy arm/disarm operations in the [API specification](/api-reference/api-reference/openapi-spec.md).
