A retest is a different document than a first pentest
"Did we find new bugs?" is the wrong question for a retest. The right one is whether the fixes hold under the same pressure that found the originals, and what still blocks a safe launch.
“Did we find new bugs” is the wrong question for a retest. The right one is: did the fixes actually hold under the same pressure that found the original problem, and what’s still standing between this system and a safe public launch. That reframing changes what you test, how you write it up, and which claims you’re willing to make, so here’s how I ran the follow-up phase on a multi-tenant CMS engagement, tenant isolation, sessions, CSRF, injection, uploads, business logic, headers, network exposure, all live.
An “authorized” test has to mean something specific
Every rule of engagement here exists because a prior engagement without them is unauditable. Testing was explicitly approved by the platform owner. Test identities were owner-supplied administrative access plus isolated tester-controlled low-privilege identities. Every created user, project, page, media object, and token belonged to the tester-controlled scope, no pre-existing tenant data, user, page, or media was modified or deleted. Rate testing used small bounded bursts against non-existent accounts only, no brute force or lockout risk to real users. Requests, responses, test-data identifiers, mutations, and cleanup steps were all recorded.
That last one matters more than it sounds. A mutation ledger exists for the same reason a SOC incident log exists: the tester’s own actions need to be as auditable as the system under test. If you can’t reconstruct what you did, you can’t hand the client a clean cleanup runbook, and “trust me, I only touched my own test data” isn’t evidence.
What actually held up
A pentest report that only lists failures quietly misleads a team into distrusting things that were genuinely fine. So the confirmed-good column gets the same rigor as the findings column, actively exercised under adversarial testing, not just read from source.
Tenant isolation held, foreign-tenant reads and mutations were denied across every tested project-scoped workflow. Vertical authorization held, low-privilege users couldn’t perform administrative user or project operations. Mass assignment resistance held, injected role, ownership, tenant, and state fields were ignored or rejected server-side. Rate limiting held, login bursts triggered throttling and weren’t bypassable via common forwarding-header spoofing. Sessions held, secure cookie attributes were present and logout invalidated the prior session server-side. Database input handling held, tested injection payloads didn’t alter query semantics anywhere in scope. Upload integrity held, oversized, empty, SVG, and HTML-disguised uploads were all rejected, and a valid image completed the full workflow including a path-traversal filename payload that got sanitized to a server-generated object name rather than reflected into the storage path.
Seven controls, actively attacked, still standing. That’s worth as much airtime as the findings below.
One finding, full depth
EDGE-PT-01, direct service exposure bypassed the intended edge boundary, high severity, verified live. The current state: additional host listeners routed directly to the application and renderer without the reverse proxy, trusted TLS, or the headers applied on the intended public path. That is an additional externally reachable service path of the kind attackers use for initial access or persistence, described directly rather than forced into a single ATT&CK technique. This is the failure mode that makes every other edge fix incomplete on its own, fixing the primary proxy configuration doesn’t protect traffic reaching the alternate listeners, sessions and content can traverse a weaker channel that nobody’s monitoring because nobody thinks it exists.
The required change is unglamorous and exactly as important as it sounds: remove unnecessary host publication, bind required services to private interfaces, restrict them by firewall, verify externally that only intended ingress remains. Closure evidence is a negative test, external and internal scans show no alternate application listener, all user traffic passes through the approved TLS and header policy. Not “we changed the config,” a scan showing the alternate path is actually gone.
Selected findings, summary depth
OBS-PT-02 (medium, verified live): unauthenticated operational telemetry exposed infrastructure metadata, host, OS, image, container-fleet information, reachable without authentication. This accelerates reconnaissance for free, an attacker gets accurate deployment topology and software inventory before doing any actual work. Move telemetry behind a monitoring boundary, bind to private interfaces, restrict to the collector network.
DEPLOY-PT-03 (medium, verified discrepancy): live behaviour differed from the reviewed source state, a security control observed running in production was absent from the source checkout provided for review. Neither source nor runtime can be treated as authoritative when that happens, and finding closure, rollback, and incident analysis all inherit that uncertainty. Tie builds to commits and immutable digests.
EDGE-PT-04 (medium, verified live): transport and browser hardening remained incomplete, an untrusted certificate model plus weakened browser security directives. Users can’t establish normal browser trust, and exploit containment on the client side is weaker than the intended configuration. Deploy a valid certificate chain first, then layer strict transport policy on top.
AUTHZ-PT-05 (medium, verified live, tester-owned data only): a multi-step workflow could be driven directly to a later state by calling a later-stage endpoint out of order, skipping an intended intermediate review step. No additional data access on its own, but it undermines a process control the business relies on. Validate the current state server-side before accepting any transition.
RATE-PT-06 and UPLOAD-PT-07 (confirmed good, verified live): distributed rate-limit bypass wasn’t achievable within tested scope, and the upload pipeline correctly rejected disguised and oversized payloads. Worth listing as finding-style entries rather than burying them only in the confirmed-controls table, because rate-limit bypass in particular was a specific, separately tracked objective given its relevance to a weak-credential condition flagged in an earlier phase.
Retest outcomes, closure tied to a live negative test
The follow-up phase confirmed the client had already remediated several earlier material issues, and the closure model stayed evidence-based the whole way through, nobody’s word taken for it. A previously anonymous object-storage read path returned access denied during retest. A direct object-storage listener was no longer reachable from the tested network position. The former trivially weak administrative password was rejected under the updated policy. The renderer was no longer reachable through its previously exposed direct listener. Revoked internal renderer credentials were rejected on subsequent use.
Every one of those is a fix confirmed under live adversarial pressure, not a fix confirmed by reading a diff. That distinction is why “fixed in source” and “fixed in production” should always be tracked as two separate claims, the same way a vulnerability-management program tracks a CVE as patched-in-source versus confirmed-patched-in-the-running-environment, two states, two different pieces of evidence.
Evidence and mutation discipline
Every state-changing test used isolated test data. The mutation ledger covered created identities, tenant workspaces, pages, media objects, renderer credentials, incomplete upload state, and cleanup ownership, paired with a cleanup runbook so the owner could remove retained test artifacts safely on request. A representative row: a draft page carrying an unsafe-link payload was left in place, never published, because the live unsafe-link control rejected the publish attempt before the page ever reached the public site. That row alone is a closed finding and a piece of test-data hygiene at the same time.
The verdict
No active critical data leak or authentication bypass turned up in the follow-up pass. Application-layer authorization and input controls performed well under active attack. The remaining release blockers concentrated almost entirely in transport trust, alternate network exposure, operational metadata, and deployment attribution, the operational layer, not the application layer.
That’s the same split I keep landing on across these engagements, the part everyone instinctively worries about with a multi-tenant CMS, authorization and input handling, was genuinely well built. The part that actually blocked launch was almost entirely: could the platform survive a bad day, and could anyone prove what was actually running in production.
TL;DR
A retest earns its name by re-attacking the exact thing that was fixed, not by scanning for anything new. Report the controls that held with the same rigor as the ones that didn’t, tie every closure claim to a live negative test rather than a source-code read, and carry at least one finding to full narrative depth so the reader can trust the summary rows. The application resisted cross-tenant access, privilege escalation, mass assignment, rate-limit bypass, and malicious uploads. It still couldn’t launch, because the edge boundary and the deployment chain hadn’t caught up to the application layer yet.