
PCI DSS v4.0.1 became the only active PCI version after 31 December 2024. I opened the PCI SSC post dated 11 June 2024. Future-dated items in that family became mandatory on 31 March 2025.A PDF that never becomes a closed ticket is the failure this page is for.
This page is how a shipping team buys and uses a human review. It is not a lab. Pair it with the secure coding checklist for the control you still have to write. The honest split between a tripwire and a person sits on scanner versus human. What evidence to demand by surface sits on types as buyer language.
A product team owns the loop
NIST SP 800-115 is still the planning document people name. I opened the CSRC page on 22 August 2026. The publication date is September 2008. It has not been superseded. It is a guide for designing a test, reading the output, and writing a mitigation plan. It is not a recipe for breaking in.
OWASP’s Web Security Testing Guide stable release is still v4.2, dated 3 December 2020. I opened the project page. Version 5.0 is listed as in development. There is no 2025 or 2026 stable WSTG. Name v4.2 in the statement of work if you want a versioned web method. Do not name “latest” and hope the PDF matches next year.
The team that ships the app owns four objects: the scope file, the two fixtures, the ticket that maps a finding to a patch, and the replay that proves the patch held. The vendor owns the hours inside the window and the writeup. If you skip the first four, you bought a PDF.
YOU commit roe.yml
mint alice@shop.example
mint bob@shop.example
name STAGING_ORIGIN + window
|
VENDOR works only inside that window
files finding PNT-041
|
YOU ticket + patch + CI replay
expect 404 on Bob GET
|
VENDOR retests PNT-041 only
you keep the request proof
Write the scope before anyone is booked
Scope is a file you commit, not a slide in a vendor deck. List every origin, every account, and every dataset the reviewer may touch. List every origin they must not touch. If a host is missing from both lists, it is out. Fail closed on the unnamed host.
Put the money flow in the file. Checkout, refund, password reset, export, admin impersonation: those are the pages a person is hired to read. A banner-grab of nginx is a scanner job. Keep that job on the tripwire page. This file is for the human hours.
# roe.yml buyer-owned. Identifiers stay as written.
engagement: checkout-web-2026q3
window:
start: "2026-09-08T00:00:00Z"
end: "2026-09-19T23:59:59Z"
in_scope:
origins:
- https://staging.shop.example
accounts:
- alice@shop.example
- bob@shop.example
data: throwaway_orders_only
out_of_scope:
- https://pay.example
- https://shop.example
- any host we do not operate
forbidden:
- denial_of_service
- social_engineering
- production_customer_rows
emergency:
slack: "#sec-oncall"
page: oncall-sec
source_ips: provided_by_vendor_48h_prior
Identifiers on this page stay roe.yml, STAGING_ORIGIN as https://staging.shop.example, alice@shop.example, bob@shop.example, and PNT-041. If the vendor’s report uses other names, map them in the ticket. Do not let a finding point at production because staging was “too locked down.”
Limits, window, and third-party hosts
The written limits are the only reason a hired review is not just unauthorized access. Name the start, the stop, the source IPs, the on-call channel, and the stop-the-test phrase. Name the environment. Staging with throwaway rows is the default. Production customer rows stay out unless legal and the data owner both signed, and even then you mint test users, you do not browse real ones.
I opened the AWS Customer Support Policy for Penetration Testing on 22 August 2026. Customers may test their own resources on a listed set of services without prior AWS approval. They may not test AWS infrastructure or AWS services themselves. Command-and-control needs prior approval. Denial of service, request flooding, and Route 53 zone walking are forbidden. Simulated phishing needs a Simulated Events form, filed at least two weeks ahead. Put those lines in roe.yml if any origin sits on AWS. Recheck the permitted-services list on the day you sign. A screenshot from last year is not authority.
Social engineering is a different product. If it is not in the file, it is forbidden. So is flooding login. So is planting persistence. You are buying a review of your app, not a foothold story for a slide.
Mint two accounts you can replay
A person who cannot log in as two users will miss the bug you actually ship: Bob reading Alice’s order. CWE-639 is a direct object reference the handler never scoped. You already know Alice’s id. You already have a staging factory. Mint alice@shop.example and bob@shop.example before day one. Hand both sessions to the vendor. Keep copies so your suite can replay the same ids after the patch.
Do not hand a shared admin cookie and call it coverage. Admin is one role. The miss is the other tenant. If the app is an API with a bearer, mint two tokens the same way. This page only cares that two subjects exist and that the report says which subject was used.
Give the vendor a seed script, not a tour. The script creates one order owned by Alice. The reviewer asks whether Bob’s session can read it. That is the hour you paid for. A spider that never logged in cannot invent that pair.
Refuse a finding that cannot be fixed
A usable finding is a ticket. It names the route, the subject, the request that was sent against your origin, the status that came back, the row that should have been hidden, and the change you can merge. CVSS is optional. A plugin id from a scanner is not a finding. “Consider adding a WAF” is not a finding.
Require this shape in the statement of work. If the first draft is a Nessus dump with a cover page, send it back before the debrief. The buyer page on types lists the evidence by surface. This page only cares that you can open PNT-041 and write a test.
# finding.pnt-041.yml reject if any required key is empty
id: PNT-041
route: "GET /orders/8412"
subject: bob@shop.example
origin: https://staging.shop.example
request_proof: attached HAR or sanitized curl against STAGING_ORIGIN
observed: "200 and Alice's order body"
expected: "404 or 403, no row"
impact: "cross-tenant read of order contents"
fix: "scope the SELECT by org_id and owner_id"
retest: vendor_confirms_404
ci: test/pentest-retest.idor.spec.js
Unconfirmed claims stay out. If the reviewer could not reproduce on staging, the row is “needs more,” not High. If they used a production URL, stop the window and rotate what they touched.
Ticket, patch, then pay for the retest
Open one ticket per finding id. Paste the route and the expected status. Point at the owner check or the bind that is missing. The IDOR guide is the WHERE clause. Do not open a “research hardening” epic and lose the id.
Hold a slice of the invoice for retest. The vendor returns only to PNT-041 and the other closed ids. They do not get a free second full review unless you buy one. You send the staging build hash. They send a short addendum: same request, new status, closed or still open.
| Step | Who | Done looks like |
|---|---|---|
| Scope file | You | roe.yml merged before kickoff |
| Hours in window | Vendor | Work only on named origins |
| Finding draft | Vendor | Every required key filled |
| Patch | You | PR + CI replay green |
| Retest | Vendor | Addendum on PNT-041 |
| Proof kept | You | Ticket, HAR, test name |
Cleanup is on the vendor: accounts they created, objects they uploaded, headers they added. You verify staging is back to the seed. You rotate any secret they were given. You do not leave a tester password in the shared vault for the next quarter.
Keep a CI proof for PNT-041
The cheap proof is the same replay the reviewer used, committed next to the happy path. You are not teaching an exploit. You are asserting Bob’s session cannot read Alice’s order after the WHERE clause landed.
// test/pentest-retest.idor.spec.js
const request = require("supertest");
const app = require("../src/app");
async function login(email, password) {
const res = await request(app).post("/login").send({ email, password });
return res.headers["set-cookie"];
}
test("PNT-041 closed: Bob GET Alice order is 404", async () => {
const aliceSid = await login("alice@shop.example", process.env.ALICE_PASS);
const created = await request(app)
.post("/orders")
.set("Cookie", aliceSid)
.send({ sku: "SKU-1", qty: 1 });
const orderId = created.body.id;
const bobSid = await login("bob@shop.example", process.env.BOB_PASS);
const replay = await request(app)
.get(`/orders/${orderId}`)
.set("Cookie", bobSid);
expect(replay.status).toBe(404);
expect(replay.body).not.toHaveProperty("sku");
});
If the handler should return 403 instead, change the assertion, not the story. Keep aliceSid, bobSid, and orderId as the fixture names. Grep the suite for PNT-041 so a future refactor cannot delete the proof and leave the ticket “done.”
rg -n "PNT-041|alice@shop.example|roe.yml" --glob '!node_modules'
A green job here is not a replacement for the next scheduled review. It is the reason this finding does not reopen on the same path. Buy the next review when the money flow changes, not when a calendar app pings.
Questions we keep getting
Do we owe the vendor a kill-chain story?
No. You owe them origins, accounts, a window, and a stop phrase. They owe you findings you can merge. Persistence, malware, and lateral movement are out unless you bought a different product and legal signed.
Can we skip staging and point them at production?
Default is no. Production customer rows stay out. If legal forces a production window, mint throwaway users, name the hours, and watch the on-call channel. Still keep https://pay.example out unless that owner wrote yes.
What if the first report is only scanner output?
Send it back. Pay for the hours after you have a finding with a request and a fix. The tripwire already lives in CI. Do not buy it twice and call the second copy a review.



