Get a quote

A complete report, annotated.

Northwind Billing is fictional and every host is a documentation domain. The structure is the one your report will follow, with a note on why each section exists.

6 findings3 assets × 4 rolesv1.1 · includes retest

Anatomy of the report

Eight sections, each there because a reader needs it.

  1. 01Cover

    States the system, version, roles and assets up front so a reviewer knows exactly what the report covers.

  2. 02Summary

    Written for your client’s or customer’s reviewer: what was tested, what was found and what is now fixed, in one minute.

  3. 03Scope record

    Shows every asset against every role, plus what was out of scope, so coverage claims can be checked.

  4. 04Method

    Names the approach, standards and environment, so the reader can judge the depth of testing.

  5. 05Findings

    Each finding states the condition, impact and fix in plain language before any technical detail.

  6. 06Evidence

    A short request and response proves the finding is real and lets your engineers reproduce it.

  7. 07Severity rationale

    Explains why each rating was given for this system, instead of a bare score.

  8. 08Verification ledger

    Records which fixes were retested and their status, which is what reviewers ask about next.

ZenoPentest · Security assessment

Northwind Billing
Web application & API

Synthetic specimen · fictional company · no client system

Report v1.1 · includes retest

01Executive summary

Why: Written for your client’s or customer’s reviewer: what was tested, what was found and what is now fixed, in one minute.

Northwind Billing asked for a manual, authenticated assessment of its invoicing web application and public API ahead of onboarding a large enterprise customer. The customer’s security review requires a recent penetration test covering tenant isolation and access control.

Testing covered three assets and four roles as recorded in the scope record. Six findings were reported: two high, two medium, one low and one informational. No critical issues were identified within the tested scope.

The most significant issue allowed a signed-in member of one customer organisation to read invoices belonging to another organisation by changing an identifier. A second high-severity issue allowed an SSO login to be linked to an existing account without confirming that the account owner controlled the identity provider.

Northwind’s engineers fixed four findings during the engagement, and retesting confirmed all four: both high-severity issues, one medium and the informational item. One medium finding is awaiting a fix, and the low finding has been accepted as a documented risk by Northwind.

For a reviewer: the tenant isolation and access-control areas named in the enterprise customer’s questionnaire were in scope, both high-severity issues in those areas are verified as fixed, and the remaining items are recorded with their status in the verification ledger.

SeverityFindings
Critical0
High2
Medium2
Low1
Info1

02Scope record

Why: Shows every asset against every role, plus what was out of scope, so coverage claims can be checked.

AssetMemberBilling adminAdminAnonymous
app.example.com
Customer web application
In scopeIn scopeNot applicableIn scope
api.example.com
Public REST API (v2)
In scopeIn scopeNot applicableConditional
admin.example.com
Internal admin console
Not applicableNot applicableIn scopeIn scope

In scope in scope Conditional conditional Not applicable not applicable

Roles

  • Member — Standard user inside one customer organisation
  • Billing admin — Manages invoices and payment settings for one organisation
  • Admin — Northwind staff role on the internal admin console
  • Anonymous — Unauthenticated visitor to public pages and API endpoints

Out of scope

  • Third-party payment provider pages and APIs
  • Northwind corporate network, email and office infrastructure
  • Denial-of-service and load testing
  • Social engineering of Northwind staff or customers
  • Mobile applications (planned for a later assessment)

03Method

Why: Names the approach, standards and environment, so the reader can judge the depth of testing.

Approach

Manual, authenticated grey-box testing with API documentation and test accounts for every in-scope role. Automated tooling supported discovery; every reported finding was confirmed manually.

Standards

Test areas mapped to OWASP WSTG, OWASP ASVS 5.0 and the OWASP API Security Top 10 (2023).

Environment

A staging environment configured to match production, with synthetic customer organisations created for testing.

Role matrix

Every sensitive action was attempted as each role and across two customer organisations to test vertical and horizontal access control.

Severity

Rated on impact and likelihood in the context of this system, with a written rationale per finding. CVSS v4 vectors informed, but did not decide, each rating.

Retest

Fixes reported by Northwind were retested against the original condition and the ledger was updated in report version 1.1.

04Findings

Why: Each finding states the condition, impact and fix in plain language before any technical detail. A short request and response proves the finding is real and lets your engineers reproduce it. Explains why each rating was given for this system, instead of a bare score.

ZP-SYN-0042HighAuthorization · BOLASynthetic example · no client system

Invoice read does not check tenant ownership

Asset
api.example.com
Caller
Member, Tenant A
Standard
OWASP API1:2023

Impact. Any signed-in customer could read another customer’s invoices, including amounts, line items and billing contacts.

Fix. Enforce tenant ownership in the invoice query itself, not only in the controller.

Evidence · request → response

GET /v2/invoices/inv_7Q2K HTTP/1.1
Host: api.example.com
Authorization: Bearer ‹tenant-a member token›
HTTP/1.1 200 OK
{ "id": "inv_7Q2K",
  "tenant": "tenant-b",
  "total": 18420.00 }

Condition

The invoice endpoint checks that the caller is signed in, but not that the requested invoice belongs to the caller’s organisation.

Observed

  1. Signed in as a Member of Tenant A.
  2. Requested an invoice identifier issued to Tenant B.
  3. The API returned the complete Tenant B invoice.

Severity rationale

High impact: confidential financial data of other customers is exposed, which directly contradicts the tenant isolation the enterprise reviewer asked about. Likelihood is moderate: identifiers are not sequential, but they appear in emailed invoice links and exported files.

Remediation

  • Scope the invoice lookup by the caller’s tenant in the data-access layer.
  • Return 404 rather than 403 for objects outside the caller’s tenant.
  • Add an automated test that requests another tenant’s invoice and expects denial.
ZP-SYN-0039HighAuthentication · SSOSynthetic example · no client system

SSO login can attach to an existing account without proof of ownership

Asset
app.example.com
Caller
Anonymous, then SSO user
Standard
OWASP ASVS V6 (Authentication)

Impact. A person controlling an identity provider tenant could gain access to an existing Northwind account that shares the same email address.

Fix. Only link an SSO identity to an existing account after the signed-in account owner confirms it, or when the organisation has verified the email domain.

Evidence · request → response

GET /auth/sso/callback?state=‹state›&code=‹code› HTTP/1.1
Host: app.example.com
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=‹new session for existing account›

Condition

The SSO callback matches incoming identities to existing accounts by email address alone and signs the user in.

Observed

  1. Configured a test identity provider with a user whose email matched an existing synthetic account.
  2. Completed SSO sign-in.
  3. The application signed in to the existing account without any confirmation step.

Severity rationale

High impact: full account access. Likelihood is lower than ZP-SYN-0042 because it requires control of an identity provider that the target organisation accepts, which the scope record identified as possible for self-service SSO setup.

Remediation

  • Require an explicit, authenticated linking step for new SSO identities.
  • Restrict automatic linking to organisations with a verified email domain.
  • Notify account owners when a new sign-in method is linked.
ZP-SYN-0044MediumAuthentication · Rate limitingSynthetic example · no client system

Password reset requests are not rate limited

Asset
app.example.com
Caller
Anonymous
Standard
OWASP API2:2023

Impact. Reset emails can be triggered in bulk, which can be used to harass users and to support guessing of short-lived reset codes.

Fix. Limit reset requests per account and per source, and lengthen reset tokens.

Evidence · request → response

POST /auth/password-reset HTTP/1.1
Host: app.example.com
{ "email": "[email protected]" }
HTTP/1.1 202 Accepted
{ "status": "sent" }

Condition

The reset endpoint accepted a sustained series of requests for the same account without any throttling or challenge.

Observed

  1. Sent repeated reset requests for a synthetic test account.
  2. Every request was accepted and a new email generated.

Severity rationale

Medium: no direct data exposure, but it degrades a security control and creates user-facing abuse. Reset tokens are six digits, which raises the likelihood if combined with guessing.

Remediation

  • Throttle per account and per source address.
  • Use long, single-use reset tokens with a short expiry.
  • Return the same response whether or not an account exists.
ZP-SYN-0045MediumAPI · Mass assignmentSynthetic example · no client system

Profile update accepts a role field

Asset
api.example.com
Caller
Member, Tenant A
Standard
OWASP API3:2023

Impact. A member could raise their own role to billing admin within their organisation and change payment settings.

Fix. Accept an explicit allow-list of editable profile fields and ignore everything else.

Evidence · request → response

PATCH /v2/me HTTP/1.1
Host: api.example.com
{ "display_name": "Sam", "role": "billing_admin" }
HTTP/1.1 200 OK
{ "display_name": "Sam", "role": "billing_admin" }

Condition

The profile endpoint binds the request body directly to the user model, including fields that should only change through the admin role-management flow.

Observed

  1. Signed in as a Member.
  2. Updated the profile with an additional role field.
  3. The role change was saved and took effect on the next request.

Severity rationale

Medium: privilege change is limited to the caller’s own organisation and requires an existing account, but it bypasses the organisation’s own role controls.

Remediation

  • Use a dedicated input schema with an allow-list of fields.
  • Move role changes to an endpoint that checks the caller is an organisation admin.
ZP-SYN-0046LowInformation exposureSynthetic example · no client system

Verbose errors reveal framework and query details

Asset
api.example.com
Caller
Member, Tenant A
Standard
OWASP WSTG-ERRH

Impact. Error responses disclose internal details that make other issues easier to find. No data exposure was observed.

Fix. Return generic error messages to clients and keep detail in server logs.

Evidence · request → response

GET /v2/invoices?sort=unknown_field HTTP/1.1
Host: api.example.com
HTTP/1.1 500 Internal Server Error
{ "error": "column \"unknown_field\" does not exist", "trace": "‹stack trace›" }

Condition

Unhandled exceptions in the API return the underlying error message and a stack trace.

Observed

  1. Sent a request with an unsupported sort parameter.
  2. The response included the database error and a stack trace.

Severity rationale

Low: assists an attacker’s reconnaissance but does not on its own expose customer data or change behaviour.

Remediation

  • Add a global error handler that returns a generic message and a correlation ID.
  • Validate sort and filter parameters against an allow-list.
ZP-SYN-0047InfoConfiguration · HeadersSynthetic example · no client system

Content Security Policy is report-only

Asset
app.example.com
Caller
Anonymous
Standard
OWASP ASVS V3 (Web frontend)

Impact. No vulnerability was identified, but an enforced policy would limit the effect of any future script injection.

Fix. Move the existing policy from report-only to enforced once reports are clean.

Evidence · request → response

GET / HTTP/1.1
Host: app.example.com
HTTP/1.1 200 OK
Content-Security-Policy-Report-Only: default-src 'self'

Condition

The application sends a Content Security Policy in report-only mode.

Observed

  1. Reviewed response headers across the application.

Severity rationale

Informational: a hardening recommendation rather than an exploitable weakness.

Remediation

  • Review collected CSP reports.
  • Switch to an enforced policy and keep reporting enabled.

05Verification ledger

Why: Records which fixes were retested and their status, which is what reviewers ask about next.

FindingSeverityStatusNote
ZP-SYN-0042
Invoice read does not check tenant ownership
HighVerified fixedRetested in v1.1: cross-tenant requests return 404; same-tenant access unaffected.
ZP-SYN-0039
SSO login can attach to an existing account without proof of ownership
HighVerified fixedRetested in v1.1: unlinked SSO identities now require confirmation from the existing account.
ZP-SYN-0044
Password reset requests are not rate limited
MediumFix pendingFix scheduled by Northwind; not yet available for retest at v1.1.
ZP-SYN-0045
Profile update accepts a role field
MediumVerified fixedRetested in v1.1: role field is ignored on profile updates.
ZP-SYN-0046
Verbose errors reveal framework and query details
LowAccepted riskAccepted by Northwind as a documented risk until the API v3 migration; recorded in their risk register.
ZP-SYN-0047
Content Security Policy is report-only
InfoVerified fixedRetested in v1.1: policy is enforced.

06Limitations

  • Testing took place in staging. Configuration differences between staging and production were reviewed with Northwind but cannot be fully excluded.
  • Findings reflect the system during the test window. Later changes are not covered unless retested.
  • Source code was not reviewed; this was a grey-box assessment with API documentation.
  • The third-party payment provider and mobile applications were out of scope.
  • A penetration test is testing evidence. It is not an attestation, certification or guarantee that no other vulnerabilities exist.

Want a report like this one?

A short form about the product and the deadline. We reply with scoping questions, then a fixed written quote.