Every security bug deserves a regression test.
ExploitSpec turns a proven HTTP exploit into a small, reviewable test that keeps the same vulnerability from coming back.
ExploitSpec is listed as an API testing tool in the OWASP™ Community API Security Tools list. The OWASP™ Word Mark is a registered or unregistered service mark of OWASP Foundation, Inc. in the United States and other countries. All rights reserved. Unauthorized use strictly prohibited. Inclusion does not imply endorsement, certification, or OWASP project status.
The repository includes a deliberately vulnerable and fixed multi-tenant API. Run the complete RED → GREEN → STABLE calibration locally with one command:
git clone https://github.com/pazent/exploitspec.git
cd exploitspec
make demoIt uses only Go and loopback ports: no account, cloud service, Docker image, or real target. The demo proves the authorization invariant fails on the vulnerable baseline, passes after the fix, and stays stable for three runs.
Prefer to inspect a passing consumer workflow? The
pazent/exploitspec-demo template
runs a complete BOLA/IDOR invariant through the published GitHub Action. The
case study explains the
actors, dynamic captures, non-leak assertions, CI wiring, and current limits.
A pentest finding is valuable on the day it is discovered. A PDF becomes stale. ExploitSpec keeps the proof alive next to the code: describe the actors, replay the request sequence, capture dynamic values, and assert the security invariant that the fix must preserve.
pentest proof -> .exploit.yaml -> repeatable local check
| passed
| failed (regression)
` execution error
ExploitSpec is deliberately narrow. It is not a vulnerability scanner and it does not claim to discover security bugs. It executes invariants that a human has already decided are meaningful.
Not sure whether the job belongs in ExploitSpec, Hurl, k6, Postman/Newman, an ordinary integration test, or a scanner? See the neutral decision guide.
- Evidence in, signal out. Tests begin with a confirmed finding, not a heuristic alert.
- Multi-actor by design. Each actor has independent headers, cookies, and an HTTP session, which makes authorization regressions natural to express.
- Dynamic workflows. Capture an ID, header, or regex match from one response and reuse it in later requests.
- Calibrated before trust. Verify that a spec fails on a vulnerable baseline, passes on the fixed baseline, and remains stable across repeated runs.
- Plain YAML. Specs are diffable, code-reviewable, and stored with the application they protect.
- Automation-friendly. Deterministic exit codes plus JSON and JUnit reports work with ordinary build systems.
- Local-first and private. No account, hosted service, analytics, or telemetry. The CLI reads local specs and connects only to targets you select.
- Free and open source. One Apache-2.0-licensed edition, without paid feature gates.
The current CLI implements:
init— create a starter.exploit.yamlwithout overwriting an existing file;import curl— turn a pasted cURL command into a redacted starter spec without executing the command;import har— list a bounded HAR capture, explicitly select one request, and generate a conservatively redacted starter spec without replaying traffic;validate— discover and strictly validate one or more specs without sending network requests;run— execute HTTP workflows and report invariant results as text, JSON, or JUnit XML;calibrate— check RED, GREEN, and repeated STABLE behavior against known vulnerable and fixed targets;- environment and captured-variable interpolation;
- status, body, regular-expression, JSON-path, and response-header assertions;
- JSON-path, response-header, and regular-expression captures;
- explicit remote-host authorization, redirect checks, response limits, timeouts, and a permanent block for known cloud metadata IPs.
See the specification for the exact supported syntax and architecture for the execution and safety model.
ExploitSpec is pre-1.0. The behavior documented here is implemented and tested, but compatibility policy and a stable release are still roadmap work.
Install the current prebuilt release on macOS or Linux through the verified Homebrew tap:
brew install pazent/exploitspec/exploitspec
exploitspec versionThe formula pins each platform archive to its published SHA-256 digest.
Alternatively, install the tagged release directly with the latest patched release of Go 1.25 or newer:
go install github.com/pazent/exploitspec/cmd/exploitspec@v0.2.0
exploitspec versionPrebuilt archives for Linux, macOS, and Windows are available on the
v0.2.0 release page,
with a SHA256SUMS file for verification.
Starting with v0.2.0, GitHub also records build-provenance attestations for every archive and the checksum manifest. After downloading an asset, verify its workflow identity with:
gh attestation verify PATH_TO_ASSET --repo pazent/exploitspecTo build from source instead (the commands below use this local binary):
git clone https://github.com/pazent/exploitspec.git
cd exploitspec
go build -o ./bin/exploitspec ./cmd/exploitspecCreate a starter test:
./bin/exploitspec init security/SEC-001.exploit.yamlSet the target and credentials used by that spec:
export EXPLOITSPEC_BASE_URL=http://127.0.0.1:8080
export OWNER_TOKEN=replace-me
export ATTACKER_TOKEN=replace-meThen validate and run it:
./bin/exploitspec validate security/
./bin/exploitspec run security/Already have a proven request copied as cURL? Import it as data:
./bin/exploitspec import curl \
--out security/SEC-042.exploit.yaml \
--id SEC-042 \
--name "Foreign invoice access stays denied" \
--json-not-exists '$.customer_email' \
< request.txtThe importer does not execute cURL or invoke a shell. It parses one supported
HTTP request, rejects shell control and command-substitution syntax, and replaces
all non-allowlisted header values, every query/form value, and every JSON string
leaf (except exact lowercase JSON Patch operation names) with required EXPLOITSPEC_*
environment variables. It refuses to overwrite an existing output file.
Import is a starting point, not automatic vulnerability discovery. Review the generated actor, target, request, redactions, expected statuses, and absence assertions before running it. Opaque body content types are rejected because the importer cannot safely redact them; write those requests manually after review.
For a HAR 1.2 capture, list value-free request summaries first, then import one explicit one-based entry:
./bin/exploitspec import har --list --file capture.har
./bin/exploitspec import har \
--file capture.har \
--entry 3 \
--out security/SEC-043.exploit.yaml \
--id SEC-043 \
--name "Captured authorization boundary stays closed"HAR import never replays traffic. It accepts at most 8 MiB and 256 entries,
requires unambiguous HAR 1.2 JSON, refuses encoded, binary, multipart, opaque,
missing, or malformed bodies, and accepts only structurally parseable JSON or
URL-encoded form data. The generated YAML replaces the origin, every path
segment, query/form name and value, allowlisted header value, JSON object key,
and JSON string leaf with an EXPLOITSPEC_HAR_* environment placeholder.
Custom header names and format controls in captured names are rejected. Only the
selected allowlisted HTTP method, a normalized supported Content-Type, fixed
protocol header names, and exact lowercase JSON Patch operation names remain
literal. Numeric, boolean, and null JSON values are rejected because string
placeholders would change their type. Remote execution still requires
run --allow-host, so importing a capture does not authorize its target.
You can also run directly from a source checkout:
go run ./cmd/exploitspec validate security/
go run ./cmd/exploitspec run security/The separate ExploitSpec Burp extension can turn explicitly selected Burp requests into redacted, reviewable starter specs. It writes locally, never replays the selected traffic, and treats every generated expectation as a draft that requires human review.
The extension is currently a prerelease public beta. Its automated tests and reproducible-build checks pass, but the packaged JAR has not yet completed the manual Burp/platform audit and has not been submitted to or approved for the BApp Store. See the extension repository for the exact safety boundary, installation steps, checksum, and current limitations.
This invariant creates an object as its owner, captures its runtime ID, then proves that another tenant cannot read it:
version: "1"
id: SEC-001
name: Cross-tenant object access is denied
description: A user must never read an object owned by another tenant.
target:
base_url: "${EXPLOITSPEC_BASE_URL:-http://127.0.0.1:8080}"
allowed_hosts:
- staging.example.com
defaults:
timeout: 5s
max_body_bytes: 1048576
headers:
Accept: application/json
actors:
owner:
headers:
Authorization: "Bearer ${OWNER_TOKEN}"
attacker:
headers:
Authorization: "Bearer ${ATTACKER_TOKEN}"
steps:
- name: Owner creates an object
actor: owner
request:
method: POST
path: /api/objects
json:
label: security-regression-canary
expect:
status: 201
capture:
object_id:
json_path: $.id
- name: Another tenant cannot read it
actor: attacker
request:
method: GET
path: /api/objects/{{ object_id }}
expect:
status: [403, 404]
json:
not_exists:
- $.owner_id
- $.sensitive_dataRun a single file against another base URL:
./bin/exploitspec run \
--base-url https://staging.example.com \
--allow-host staging.example.com \
security/SEC-001.exploit.yamlLoopback targets are allowed automatically. Every non-loopback hostname must be
authorized in target.allowed_hosts or with --allow-host.
A regression test is trustworthy only if it distinguishes the vulnerable and
fixed behaviors. calibrate performs that check in three stages:
- RED: the invariant must fail against a known-vulnerable baseline.
- GREEN: the invariant must pass against the fixed target.
- STABLE: the fixed target must keep passing for the requested number of runs.
./bin/exploitspec calibrate \
--vulnerable-url http://127.0.0.1:8081 \
--fixed-url http://127.0.0.1:8082 \
--runs 5 \
security/SEC-001.exploit.yamlCalibration reports the observation; it does not modify or certify the spec. The vulnerable and fixed systems, their state, and their test data remain your responsibility.
Human-readable output is the default:
./bin/exploitspec run security/JSON summary:
./bin/exploitspec run --format json --output exploitspec-report.json security/JUnit XML:
./bin/exploitspec run --format junit --output exploitspec-report.xml security/Files written with --output are forced to owner-only permissions (0600) and
cannot overwrite an input spec, including through a symlink. Reports avoid
including response bodies and captured values, redact templated body assertion
values, and do not print compared JSON or header values. Treat reports as
potentially sensitive anyway: they still contain spec metadata, source paths,
assertion structure, and diagnostic text.
| Exit code | Meaning |
|---|---|
0 |
Every invariant passed |
1 |
At least one invariant failed |
2 |
Invalid input, rejected target, or execution/reporting error |
Install ExploitSpec from the GitHub Marketplace, or reference the composite action directly. Pin it to the immutable v0.2.0 commit, keep the workflow token read-only, and point it at a test target whose hostname is authorized by the spec:
name: Security regressions
on: [pull_request]
permissions:
contents: read
jobs:
exploitspec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pazent/exploitspec@ec62275648ed834bf44324bf86aafd3be3f2fee4 # v0.2.0
with:
specs: security/exploits
base-url: https://staging.example.com
format: junit
report: artifacts/exploitspec.xmlThe action runs the same deterministic local CLI and creates the selected report inside the workspace. It does not upload specs, responses, or telemetry.
exploitspec init PATH.exploit.yaml
exploitspec import curl --out PATH.exploit.yaml < request.txt
exploitspec import har --list < capture.har
exploitspec import har --entry N --out PATH.exploit.yaml < capture.har
exploitspec validate [PATH...]
exploitspec run [FLAGS] [PATH...]
exploitspec calibrate --vulnerable-url URL --fixed-url URL [FLAGS] SPEC
exploitspec version
Directories are searched recursively for these suffixes:
.exploit.yaml.exploit.yml.exploitspec.yaml.exploitspec.yml
Run exploitspec help, or read the
full specification, for behavior and limits.
ExploitSpec sends the HTTP methods and bodies defined in a spec. A test can create, change, or delete data. Run it only against systems you own or are explicitly authorized to test, preferably an isolated test environment with synthetic data and least-privilege credentials.
Built-in controls reduce accidents, but they are not a sandbox:
- remote hosts require explicit authorization;
- redirects are checked against the same policy and stop after five hops;
- credential-bearing headers are removed on cross-origin redirects;
- URL credentials and non-HTTP(S) schemes are rejected;
- URL fragments are rejected because they are not sent in HTTP requests;
169.254.169.254,fd00:ec2::254, and canonical aliases are always blocked;- request timeouts and bounded response bodies are enabled;
- TLS 1.2 or newer is required; certificate verification can only be disabled
explicitly with
--insecure.
The allowlist is hostname-based. It is not DNS rebinding protection, an IP-range policy, or an authorization grant. Review every spec before execution.
ExploitSpec has no telemetry and no control plane. It does not upload specs,
credentials, results, or usage data to the ExploitSpec project. During run and
calibrate, it makes the HTTP requests described by the spec to the selected
targets. Go's standard proxy environment settings may route those requests
through a configured proxy.
The CLI does not automatically load .env files. The included
.env.example is a shell-oriented template; export its values
yourself or use your preferred secret manager.
ExploitSpec optimizes for durable public value and technical credibility:
- one free, open-source edition;
- local execution without an account;
- no telemetry;
- explicit, inspectable behavior over autonomous exploitation;
- small specs that can be reviewed beside application code;
- honest security claims backed by executable evidence.
The roadmap is public. Contributions, critical review, reproducible bug reports, and real-world specs with sanitized data are welcome. Start with CONTRIBUTING.md.
To report a vulnerability in ExploitSpec itself, follow SECURITY.md. Do not disclose sensitive details in a public issue.
ExploitSpec is intended for defensive testing on authorized systems. You are responsible for target authorization, test data, credentials, and the effects of the requests you execute.
If ExploitSpec supports research, a publication, or public engineering work, please cite it using CITATION.cff. Public case studies, sanitized specs, technical feedback, stars, and contributions are the most useful ways to help a free project reach more people.
Apache License 2.0. See LICENSE.