Developer Security Report
Northwind Bakery Supply Co.: Wholesale Ordering Portal
example.com, .example). Every request, response, and proof of concept below is illustrative, was written for this sample, and does not work against any real system. Proofs of concept are redacted at the point of exploitation on purpose, which also reflects how working client reports are written.
| Application URL(s) | https://app.example.com, https://api.example.com, https://admin.example.com |
| Assessment type | Manual Web Application Security Assessment |
| Testing window | September 14, 2026 to September 18, 2026 |
| Report date | September 25, 2026 |
| Report version | 1.0 |
| Assessor | Isaiah R. (9mmPterodactyl) |
| Companion document | Executive Report (non-technical summary for owners and decision makers) |
1. Findings Overview
Six findings were identified. The most urgent are an SSRF in the logo import feature (F-01), an IDOR exposing every customer's invoices (F-02), and a chained open redirect and blind XSS that reaches the staff admin portal without an account (F-03).
| Severity (CVSS-B) | Count |
|---|---|
| Critical | 0 |
| High | 2 |
| Medium | 3 |
| Low | 1 |
| Total | 6 |
| ID | Title | Severity (CVSS-B) | Assessor Priority | Affected Component | Status |
|---|---|---|---|---|---|
| F-01 | Server-side request forgery in logo import | High | POST /api/v1/company/logo-from-url | Open | |
| F-02 | IDOR exposes any customer's invoices | High | GET /api/v1/invoices/{id} | Open | |
| F-03 | Chain: open redirect enables blind XSS in admin portal | High | GET /logout?next=, support ticket link preview | Open | |
| F-04 | Single-use coupon can be redeemed repeatedly | Medium | POST /api/v1/cart/coupon | Open | |
| F-05 | Stored XSS in display name on shared orders | Medium | PATCH /api/v1/me, order activity feed | Open | |
| F-06 | Missing HSTS and anti-framing headers | Low | app.example.com, admin.example.com | Open |
2. Scope
Scope was agreed in writing on September 9, 2026, before testing began.
In scope
| Asset | Notes |
|---|---|
https://app.example.com | Customer ordering portal, authenticated and unauthenticated |
https://api.example.com | REST API used by the customer portal |
https://admin.example.com | Staff admin portal, tested with a provided staff test account |
Test accounts provided: Customer A (test-a@tester.example, team "Test Café A"), Customer B (test-b@tester.example, team "Test Café B"), Customer C (test-c@tester.example, team "Test Café A"), and one staff admin account.
Environment: Production. Northwind provided a single-use test coupon (TESTONCE10). All test orders were placed against test accounts and cancelled by Northwind staff at the end of each day. The F-03 admin-portal test was coordinated in advance with Northwind's support lead, who opened the test ticket.
Out of scope
- Denial-of-service (DoS) or load testing
- Social engineering (phishing, phone pretexting)
- Physical security
- Third-party services not owned by Northwind: the payment processor and the email delivery provider
- Infrastructure or network-level exploitation beyond the application
- Live payment transactions (checkout was tested up to the payment step with test accounts using invoice billing)
3. Methodology
Testing was manual and aligned to the OWASP Top 10:2025, with test cases drawn from the OWASP Web Security Testing Guide. API testing also referenced the OWASP API Security Top 10 (2023).
Tools were used to support the manual testing, for example an intercepting proxy to view and modify requests. Every finding in this report was confirmed by hand.
All testing was non-destructive. No denial-of-service testing was performed. Out-of-band checks sent only a unique hostname to a tester-controlled listener (oob.tester.example) to confirm whether a callback occurred. They did not capture cookies, page content, or customer data. Where server-side requests could reach internal services (F-01), testing stopped at confirming reachability. No credentials or secrets were retrieved.
Areas tested
| Area | Examples of what was tested |
|---|---|
| Authentication and sessions | Login, logout, password reset, session lifetime and invalidation, cookie flags |
| Access control | IDOR across invoices, orders, addresses and team membership; customer-to-staff privilege escalation; function-level authorization on admin API routes |
| Injection | Reflected, stored, DOM, and blind XSS; SQL injection in search, filters, and login; SSRF in URL-accepting features |
| Business logic | Coupon handling, quantity and price manipulation, order workflow state changes |
| API security | Object- and function-level authorization, mass assignment on profile and order updates, excessive data exposure |
| Misconfigurations and exposed data | Security headers, TLS, verbose errors, exposed files, CORS policy, open redirects |
4. Severity Rating Scheme
Findings are rated with CVSS 4.0 (FIRST specification). Scores in this report are base scores (CVSS-B). Threat and environmental metrics were not applied. Each finding includes its full vector string so the score can be reproduced in the FIRST CVSS 4.0 calculator.
| Severity | CVSS 4.0 score |
|---|---|
| Critical | 9.0 – 10.0 |
| High | 7.0 – 8.9 |
| Medium | 4.0 – 6.9 |
| Low | 0.1 – 3.9 |
| None | 0.0 |
Assessor priority: CVSS-B rates a single vulnerability in isolation. Where chaining, business context, or real-world exploitability makes the practical risk higher or lower than the CVSS-B rating, the finding also states an assessor priority and the reason. The CVSS-B score is always shown unchanged. In this report, only F-03 has a priority that differs from its CVSS-B severity.
Issues that are not directly exploitable are listed in Section 6 as observations and are not scored.
5. Detailed Findings
F-01: Server-Side Request Forgery in Logo Import
| Severity | |
| CVSS-B score | 8.3 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N |
| Assessor priority | High (same as severity) |
| Affected endpoint | POST /api/v1/company/logo-from-url (Company profile → "Import logo from URL") |
| Status | Open |
Description
The "Import logo from URL" feature accepts a URL from any logged-in customer and fetches it from the API server. The server does not restrict the destination. It accepts http:// URLs, private and link-local IP addresses, and loopback addresses. When the fetched content is not a valid image, the error message includes the first bytes of the response body, which turns a blind SSRF into one where part of the response can be read.
Steps to Reproduce
- Log in as Customer A and open Company profile → Import logo from URL.
- Submit a valid public image URL and confirm the logo is imported (baseline).
- Intercept the request and change
urlto the cloud instance metadata address shown in the proof of concept. - Observe that the
422error response contains data returned by the internal metadata service. - Expected result: the request is rejected before any connection is made to an internal address.
Proof of Concept
Request
POST /api/v1/company/logo-from-url HTTP/1.1
Host: api.example.com
Authorization: Bearer <customer-A-test-token>
Content-Type: application/json
{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
Response
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{"error":"Image could not be processed","detail":"cannot identify image file: b'northwind-app-role\\n'"}
The internal metadata service responded, confirming the server-side request reached it. Per the rules of engagement, testing stopped at this point. No request was made for the role's credentials. A separate request to http://127.0.0.1:8080/ also returned part of an internal status page in the error detail, confirming loopback services are reachable.
Impact
Any customer account can make the API server send requests to internal network locations and read part of the response. On this deployment, an attacker could reach the cloud metadata service and internal-only services on the loopback interface. Depending on the instance role's permissions, this could lead to exposure of stored data and other cloud resources beyond the application.
Remediation
- Restrict destinations. Allow only
https://URLs. Resolve the hostname on the server and reject any address in loopback, private (RFC 1918), link-local (169.254.0.0/16), or other reserved ranges, for both IPv4 and IPv6. Connect to the IP address you validated, not a fresh lookup, to prevent DNS rebinding. - Do not follow redirects, or re-apply the same checks to every redirect target.
- Do not return fetched content in errors. Return a generic "could not import image" message.
- Harden the metadata service. Require session-token-based metadata access (for example, IMDSv2 on AWS) and reduce the instance role to the minimum permissions it needs.
- Consider removing the feature and accepting direct file uploads only.
- Verify: repeat the proof of concept and confirm the request is rejected without the server making the connection.
References
- OWASP: A01:2025 Broken Access Control (SSRF is included in this category in the 2025 edition)
- OWASP: API7:2023 Server Side Request Forgery
- CWE: CWE-918: Server-Side Request Forgery (SSRF)
- OWASP SSRF Prevention Cheat Sheet
F-02: IDOR Exposes Any Customer's Invoices
| Severity | |
| CVSS-B score | 7.1 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N |
| Assessor priority | High (same as severity) |
| Affected endpoint | GET /api/v1/invoices/{id} (Billing → Invoices) |
| Status | Open |
Description
The invoice endpoint returns any invoice whose ID is supplied. It confirms the requester is logged in but does not check that the invoice belongs to the requester's team. Invoice IDs are sequential integers, so every invoice in the system can be enumerated.
Steps to Reproduce
- Log in as Customer B and note one of B's invoice IDs (
58813in this test). - Log out and log in as Customer A, who belongs to a different team.
- Send
GET /api/v1/invoices/58813with Customer A's token. - Observe that Customer B's invoice is returned in full.
- Expected result:
403 Forbiddenor404 Not Found.
Proof of Concept
Request (Customer A requesting Customer B's invoice)
GET /api/v1/invoices/58813 HTTP/1.1
Host: api.example.com
Authorization: Bearer <customer-A-test-token>
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 58813,
"team": "Test Café B",
"billing_contact": "test-b@tester.example",
"billing_address": "[test address B]",
"lines": [
{"sku": "FLR-25KG", "description": "Bread flour, 25 kg", "qty": 4, "unit_price": 31.50}
],
"total": 126.00,
"payment_terms": "Net 30"
}
Only test accounts were accessed. Requests to neighboring IDs were limited to IDs belonging to the provided test accounts.
Impact
Any customer can read every other customer's invoices. This exposes business names, contact emails, billing addresses, order history, and negotiated pricing. A competitor posing as a customer could harvest Northwind's full customer list and pricing. The exposure may also create notification obligations under applicable privacy laws.
Remediation
- Enforce ownership on the server for every object lookup. Scope the database query to the caller's team, for example
WHERE id = :id AND team_id = :caller_team_id, and return404when no row matches. - Apply the same check to every endpoint that takes an object ID. During testing, orders, addresses, and team members were checked and found to be properly scoped. Invoices were the exception.
- Optionally use non-sequential identifiers (UUIDs) to make enumeration harder. This is defense in depth, not a fix on its own.
- Add an automated test that requests another team's invoice and expects
404. - Verify: repeat the proof of concept and confirm a
404response.
References
- OWASP: A01:2025 Broken Access Control
- OWASP: API1:2023 Broken Object Level Authorization
- CWE: CWE-639: Authorization Bypass Through User-Controlled Key
F-03: Chain: Open Redirect Enables Blind XSS in Admin Portal
| Severity | |
| CVSS-B score | 6.4 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N |
| Assessor priority | High. No account is needed to submit the payload, and it ultimately runs in the browsers of staff who have full administrative access. |
| Affected components | 1. GET /logout?next= on app.example.com — open redirect2. POST /support/tickets, "Link to the page with the problem" field — unauthenticated input3. Ticket link preview on admin.example.com — blind XSS sink |
| Status | Open |
Description
This is a chain of two issues that are low impact on their own but serious together.
- Open redirect. The logout endpoint takes a
nextparameter and sends the browser to it after logout without checking that it points back to Northwind. It will redirect to an external site. - Blind stored XSS via the support form. The public "Report a problem" form has a field for the URL where the problem occurred. The value is stored and later rendered in the staff admin portal as a clickable link preview. The admin portal does not encode the stored value when it builds that preview, so markup in the field becomes live markup in the staff page. The customer-facing site has a Content-Security-Policy that blocks inline script; the admin portal does not, so the payload executes there.
An attacker submits the form anonymously with a crafted value in the URL field. The value uses the open redirect so the rendered link appears to point at the legitimate app.example.com domain, which makes it look trustworthy to the staff reviewer. When a staff member opens the ticket, the stored payload runs in their authenticated admin session.
Steps to Reproduce
- Confirm the open redirect. Request
GET /logout?next=https://oob.tester.example/onapp.example.comand observe a302whoseLocationis the external URL (see redacted proof of concept). - Confirm the admin sink. With Northwind's support lead, submit a test ticket whose URL field contains a benign out-of-band marker instead of a working payload (a reference to
https://oob.tester.example/f03, no script). The support lead opens the ticket inadmin.example.com. - Observe that the stored value is placed into the admin page's link-preview markup without encoding, and that the out-of-band marker is requested when the page renders. This confirms attacker-controlled input reaches a script-execution context in the admin portal.
- Expected result: the logout endpoint refuses external destinations, and the admin portal encodes stored ticket values so they cannot become markup.
Proof of Concept
Step 1 — open redirect (illustrative)
GET /logout?next=https://oob.tester.example/ HTTP/1.1
Host: app.example.com
HTTP/1.1 302 Found
Location: https://oob.tester.example/
Step 2 — admin sink (verification marker, non-executing)
The support ticket's URL field was submitted with a plain out-of-band marker (https://oob.tester.example/f03) and no script. When the support lead opened the ticket, the admin portal placed the stored value directly into the link-preview markup rather than encoding it, and the marker URL was requested by the staff browser as the page rendered. That callback, from a value supplied anonymously through the public form, confirms the injection point reaches a script-execution context in the admin portal. The weaponized payload is intentionally omitted from this report.
Impact
An unauthenticated attacker can get code to run in a logged-in administrator's browser. In an admin session that could mean reading or changing any data the admin can reach, creating or modifying staff accounts, or acting as the admin within the portal. The open redirect also supports phishing by making attacker links appear to originate from Northwind's own domain.
Remediation
- Fix the open redirect. Do not redirect to arbitrary
nextvalues. Allow only relative paths, or match against an allow-list of known internal URLs. Reject absolute URLs to other hosts. - Encode output in the admin portal. Treat all ticket content as untrusted and contextually encode it when rendering. Do not build link previews from raw stored input.
- Add a Content-Security-Policy to the admin portal that forbids inline script, matching the policy already present on the customer site. This is defense in depth, not a substitute for encoding.
- Validate the URL field on submission: accept only
http/httpsURLs and store them as data, never as markup. - Verify: repeat steps 1–3 and confirm the redirect is refused and the stored marker appears as inert text in the admin portal with no callback.
References
- OWASP: A05:2025 Injection (XSS is categorized under Injection in the 2025 edition)
- CWE: CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- CWE: CWE-601: URL Redirection to Untrusted Site (Open Redirect)
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP Unvalidated Redirects and Forwards Cheat Sheet
F-04: Single-Use Coupon Can Be Redeemed Repeatedly
| Severity | |
| CVSS-B score | 5.3 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Assessor priority | Medium (same as severity) |
| Affected endpoint | POST /api/v1/cart/coupon (Checkout → Apply coupon) |
| Status | Open |
Description
Coupons flagged as single-use are only checked at the moment they are applied to a cart. The redemption is not recorded against the account or the coupon until the order is placed, and the check is not re-run atomically at that point. By applying the same coupon to several carts, or removing and re-applying it, the discount can be redeemed more than the intended once.
Steps to Reproduce
- Log in as Customer A and add items to the cart.
- Apply the single-use test coupon
TESTONCE10. The discount is accepted. - Remove the coupon, then apply it again. It is accepted again.
- Repeat with a second cart in a parallel session using the same account. Both carts accept the coupon.
- Expected result: after the first successful application tied to the account, further attempts are rejected.
Proof of Concept
Request (same coupon, second cart)
POST /api/v1/cart/coupon HTTP/1.1
Host: api.example.com
Authorization: Bearer <customer-A-test-token>
Content-Type: application/json
{"cart_id":"cart-2","code":"TESTONCE10"}
Response
HTTP/1.1 200 OK
Content-Type: application/json
{"code":"TESTONCE10","applied":true,"discount":"10%","message":"Coupon applied"}
No orders were completed. Testing confirmed repeated acceptance at the apply step only, using the provided test coupon.
Impact
Promotional and one-time discount codes can be reused, causing revenue loss. A shared or leaked "first order" code could be redeemed at scale. The integrity impact is limited to discount and pricing data, not to other customers' data.
Remediation
- Enforce the limit at order placement, inside the same database transaction that creates the order, using a unique constraint on (coupon, account) or a decrementing redemption counter with row locking.
- Re-validate the coupon server-side at placement; do not trust the discount captured at the apply step.
- Handle concurrent requests so two simultaneous orders cannot both redeem the last use.
- Verify: attempt a second redemption and a parallel redemption and confirm both are rejected at placement.
References
- OWASP: A06:2025 Insecure Design
- CWE: CWE-841: Improper Enforcement of Behavioral Workflow
- CWE: CWE-837: Improper Enforcement of a Single, Unique Action
F-05: Stored XSS in Display Name on Shared Orders
| Severity | |
| CVSS-B score | 5.1 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N |
| Assessor priority | Medium (same as severity) |
| Affected endpoint | PATCH /api/v1/me (display name), rendered in the order activity feed |
| Status | Open |
Description
A user's display name is stored as entered and rendered without output encoding in the order activity feed. Teammates on the same order, and staff who view the order, see the feed. Markup placed in the display name becomes live markup in those viewers' pages. Unlike F-03, this requires a logged-in account and only reaches users who share an order with the attacker, so its reach is narrower.
Steps to Reproduce
- Log in as Customer A and set the display name via
PATCH /api/v1/meto a value containing HTML markup (see redacted proof of concept). - Perform an action on a shared order so an entry is written to the activity feed.
- Log in as Customer C (same team, shares the order) and open the order.
- Observe that the display name is rendered as markup rather than text.
- Expected result: the display name appears as literal text.
Proof of Concept
Request (set display name — marker shown, not a working payload)
PATCH /api/v1/me HTTP/1.1
Host: api.example.com
Authorization: Bearer <customer-A-test-token>
Content-Type: application/json
{"display_name":"Test A [HTML-MARKUP-MARKER]"}
When Customer C opened the shared order, the stored value was inserted into the activity-feed markup without encoding. A benign marker (a styled text node, no script) rendered as markup rather than as text, confirming the injection point. The weaponized payload is intentionally omitted.
Impact
A team member can run script in the sessions of teammates and of staff who view a shared order. Within those sessions this could alter the displayed page or act on the viewer's behalf in the application. Reach is limited to users who share an order with the attacker.
Remediation
- Contextually encode the display name (and all user-controlled fields) when rendering them. Render as text, not markup.
- Add a Content-Security-Policy that forbids inline script to the customer application views that show the feed.
- Optionally constrain display-name characters, as defense in depth only.
- Verify: set a marker value and confirm it renders as literal text in every view that shows the display name.
References
- OWASP: A05:2025 Injection
- CWE: CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- OWASP Cross Site Scripting Prevention Cheat Sheet
F-06: Missing HSTS and Anti-Framing Headers
| Severity | |
| CVSS-B score | 2.1 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N |
| Assessor priority | Low (same as severity) |
| Affected endpoint | app.example.com and admin.example.com responses |
| Status | Open |
Description
Responses from both the customer and admin sites are missing Strict-Transport-Security and X-Frame-Options (or an equivalent frame-ancestors directive in Content-Security-Policy). Without HSTS, a browser can be downgraded to http before the redirect to https. Without anti-framing, the pages can be embedded in a hostile frame, which supports clickjacking.
Steps to Reproduce
- Request the home page of each site over HTTPS.
- Inspect the response headers.
- Observe that
Strict-Transport-SecurityandX-Frame-Options/frame-ancestorsare absent. - Expected result: both headers are present with safe values.
Proof of Concept
Response headers observed
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
Set-Cookie: session=<redacted>; Secure; HttpOnly; SameSite=Lax
# No Strict-Transport-Security header
# No X-Frame-Options header and no frame-ancestors in CSP
Impact
These are hardening gaps rather than directly exploitable flaws. Absent HSTS, a network-positioned attacker could attempt an HTTPS downgrade. Absent anti-framing, a clickjacking page could trick a logged-in user into clicking hidden controls. Impact is limited and requires specific conditions.
Remediation
- Add
Strict-Transport-Security: max-age=31536000; includeSubDomainsonce HTTPS is confirmed across all subdomains. Consider preload after verifying. - Add
X-Frame-Options: DENY(orContent-Security-Policy: frame-ancestors 'none') to both sites, relaxing only where framing is genuinely required. - Verify: re-request and confirm both headers are present.
References
- OWASP: A02:2025 Security Misconfiguration
- CWE: CWE-1021: Improper Restriction of Rendered UI Layers or Frames
- CWE: CWE-319: Cleartext Transmission of Sensitive Information
- OWASP HTTP Security Response Headers Cheat Sheet
6. Observations and Best Practice Recommendations
These items are not directly exploitable on their own, so they are not scored. Addressing them strengthens the application's overall security.
| # | Observation | Recommendation |
|---|---|---|
| O-01 | API error responses on api.example.com include a stack trace and framework version on 500 errors. | Return a generic error body. Log details server-side only. |
| O-02 | The admin portal has no Content-Security-Policy (related to F-03). | Add a restrictive CSP that forbids inline script. |
| O-03 | Session cookies are sound, but the session does not expire on server side after password change. | Invalidate all existing sessions when a password is changed. |
Positive findings (controls that worked well):
- Session cookies are set with
Secure,HttpOnly, andSameSite=Lax. - SQL injection attempts against search, filters, and login were handled safely; the application uses parameterized queries in the paths tested.
- Order, address, and team-member endpoints correctly enforced per-team authorization (the invoice endpoint in F-02 was the exception).
- The customer site enforces a Content-Security-Policy that blocked inline script during testing.
7. Limitations and Assumptions
This assessment is a point-in-time evaluation based on the agreed scope and testing window.
- Testing was time-boxed to September 14–18, 2026.
- Complex or rarely used workflows may have received limited coverage.
- No denial-of-service or destructive testing was performed.
- No source code review was performed; testing was black-box with the provided test accounts.
- Checkout was exercised only up to the payment step, as live payment processing was out of scope.
Absence of a finding does not guarantee absence of a vulnerability.
8. Retest
A retest of all reported findings is included in this engagement. When fixes have been deployed, contact the assessor to schedule it. Each finding's status will be updated in a revised version of this report and of the Executive Report.
| ID | Retest date | Result | Notes |
|---|---|---|---|
| F-01 | [pending] | [pending] | |
| F-02 | [pending] | [pending] | |
| F-03 | [pending] | [pending] | |
| F-04 | [pending] | [pending] | |
| F-05 | [pending] | [pending] | |
| F-06 | [pending] | [pending] |
This report is confidential and intended only for Northwind Bakery Supply Co. and its development team. (Sample document; Northwind Bakery Supply Co. is fictional.)