Skip to content
Unlisted / Security
Sample report · illustrative

Acme Protocol — Full-system review

A representative engagement report demonstrating the structure, severity calibration, and level of detail you can expect from an Unlisted Security review. All findings, components, and identifiers in this document are fictional.

Status · FinalEngagement · USR-2025-Q4-007v1.2
Engagement at a glance
Duration
4 weeks
Reviewers
2
Findings
10
Style
Adversarial, manual
Scope
  • vault.sol, strategy.sol, oracle.sol
  • settlement-service (Node / TypeScript)
  • admin-dashboard (Next.js)
  • infra/terraform/* and .github/workflows/*
Critical
1
High
3
Medium
3
Low
1
Informational
2
Detailed findings

Every finding ships with impact, exploit path, and a fix.

CRITICALUSR-001Smart ContractFixed
vault.solfeePlugin.sol

Cross-contract reentrancy in vault settle() via fee plugin callback

Summary

The vault settles balances after invoking a user-controlled fee plugin. An attacker can register a malicious plugin that re-enters settle() before the vault state is updated, draining funds across positions.

Impact

A single attacker can drain the vault of all active positions in a single transaction. Loss is bounded only by available liquidity at the time of attack.

Exploit path
  1. 01Deploy malicious feePlugin contract whose hook callback re-enters Vault.settle().
  2. 02Open a small position in the vault to qualify as a registered user.
  3. 03Trigger settle() — the vault calls plugin.onSettle() before updating internal accounting.
  4. 04Plugin re-enters settle() with crafted parameters, repeatedly draining shares before balance writes complete.
Recommendation

Apply checks-effects-interactions: write final balances and update internal accounting before invoking any external plugin. Add a non-reentrant guard on settle() and on every external function that mutates shares. Consider isolating plugin execution via a stateless adapter contract to remove the call from the trust boundary.

References
  • SWC-107
  • ConsenSys Reentrancy Patterns
HIGHUSR-002BackendFixed
api/admin/impersonate.tslib/session.ts

Auth bypass via stale impersonation session in admin dashboard

Summary

Admin impersonation issues a session inheriting the target user's role without re-validating the operator's permission scope on subsequent requests. After admin role is downgraded or revoked, the impersonated session retains elevated permissions until natural expiry.

Impact

A removed or downgraded admin retains a working session with elevated privileges. In the worst case, a former employee can continue to act as any user for up to the session TTL (24h).

Exploit path
  1. 01Operator with admin role calls /api/admin/impersonate?user=victim.
  2. 02Backend issues a session JWT with role=victim.role and impersonator=operator.
  3. 03Operator's admin role is later revoked.
  4. 04Operator continues to use the impersonated session, which is never revalidated against the operator's current permissions.
Recommendation

Re-validate operator scope on every request that uses an impersonated session. Persist impersonation as a server-side state (not just JWT claim) and invalidate immediately on operator role change. Cap impersonation TTL to minutes, not hours, and require explicit re-authentication for impersonation.

HIGHUSR-003BackendFixed
workers/withdraw.tslib/limits.ts

Withdrawal limit race during retried requests

Summary

The daily withdrawal limit is checked before acquiring the row-level lock on the user balance. Two concurrent withdrawals can each see the pre-debit limit and pass validation, allowing the user to withdraw up to 2x their daily cap.

Impact

Per-user daily withdrawal limits can be bypassed by submitting two requests near-simultaneously. For high-tier users, this allows withdrawals materially in excess of risk-controlled limits.

Exploit path
  1. 01Submit withdrawal A for amount = limit.
  2. 02Within ~50ms, submit withdrawal B for amount = limit from a parallel client.
  3. 03Both requests read the same pre-debit limit value before either has acquired the balance row lock.
  4. 04Both pass the limit check and proceed to debit, executing 2x the daily cap.
Recommendation

Acquire the row-level lock on the user balance before evaluating limit. Compute remaining limit inside the same transaction. Add an idempotency key on the withdrawal API and reject duplicate-or-concurrent requests at the edge.

HIGHUSR-004Smart ContractAcknowledged
oracle.solsettlement.sol

Oracle staleness and deviation checks missing on settlement

Summary

Settlement reads the oracle price without verifying timestamp staleness or deviation against the previous round. During an oracle stall or sudden depeg, settlement uses an arbitrarily old or manipulated price.

Impact

During market stress (the exact time correctness matters most), settlement may use stale prices. An attacker who can predict a brief oracle stall can extract value from open positions at favorable, stale prices.

Exploit path
  1. 01Identify oracle update cadence and historical stall windows.
  2. 02Open a position immediately before a known oracle update.
  3. 03Trigger settlement during the stall and capture the stale price.
  4. 04Close the offsetting position once the oracle resumes, capturing the price delta.
Recommendation

Require updatedAt to be within a configurable max-age (e.g. 30 minutes for slow assets, 1 minute for fast). Add a max-deviation guard against the previous round. Pause settlement on oracle failure rather than continuing on stale data.

MEDIUMUSR-005InfrastructureFixed
terraform/uploads.tf

ListBucket permitted on signed-upload prefix

Summary

The S3 bucket used for signed user uploads grants s3:ListBucket to anonymous principals on the /tmp/* prefix. While individual objects use random keys, the listing exposes their names and enables enumeration of all in-flight uploads.

Impact

Anonymous parties can enumerate all uploaded objects, including filenames that often leak user identities, internal IDs, and document types. Enables targeted scraping of in-flight content before it's processed.

Exploit path
  1. 01Issue an unauthenticated GET to the bucket with prefix=tmp/.
  2. 02Receive a paginated XML listing of every in-flight upload object.
  3. 03Fetch each object directly using its now-known key.
Recommendation

Remove ListBucket from the public principal set entirely. Make the bucket fully private and serve uploads through pre-signed GET URLs only. Apply a bucket policy that denies any anonymous request as a backstop.

MEDIUMUSR-006InfrastructureFixed
.github/workflows/deploy.ymliam/github-oidc-trust.tf

PR runners can assume production deploy role via OIDC

Summary

The GitHub OIDC trust policy on the production deployment role conditions on repository but not on workflow or branch. Any workflow in the repository — including PR-triggered workflows on untrusted forks — can request a token assuming the production role.

Impact

A malicious PR can run a workflow that obtains production AWS credentials and deploys arbitrary code or exfiltrates secrets. This is a full supply chain compromise of production from any PR author.

Exploit path
  1. 01Open a PR adding or modifying a workflow that requests an OIDC token for the production role.
  2. 02Workflow executes on PR with elevated privileges and obtains production credentials.
  3. 03Use credentials to deploy a malicious release or exfiltrate data.
Recommendation

Constrain the OIDC trust policy on sub: claim to specific protected branches (e.g. refs/heads/main and tagged releases), and to specific workflow files. Require environment protection rules with mandatory reviewers for production deploys.

MEDIUMUSR-007ApplicationOpen
app/admin/users/search/route.ts

Admin search exposes soft-deleted PII

Summary

The admin user-search endpoint queries the users table without filtering on deleted_at. Soft-deleted user records — including email and KYC document IDs — remain searchable indefinitely after account deletion.

Impact

Soft-deleted users are still discoverable via admin search, defeating the purpose of soft deletion and creating GDPR/CCPA exposure for deletion requests that have already been honored at the user-facing level.

Exploit path
  1. 01Authenticate as any admin.
  2. 02Call /admin/users/search with an email or KYC ID belonging to a deleted user.
  3. 03Receive the full record including PII intended to have been removed from access.
Recommendation

Filter on deleted_at IS NULL in the search query. Add a separate audited path for legitimate access to deleted records, with explicit reason logging and tighter ACL.

LOWUSR-008ApplicationOpen
app/auth/reset/route.ts

Insufficient rate limiting on password reset endpoint

Summary

The password reset endpoint rate limits per IP at 60 requests per minute. Distributed clients can trigger reset emails for arbitrary users at scale, enabling targeted email-flooding harassment and engagement-cost attacks.

Impact

Attackers can flood a target user's inbox with legitimate password reset emails, and impose cost on the email provider relationship. No direct account takeover impact.

Exploit path
  1. 01From a small distributed pool, request password resets for the same target email at 60 rpm per IP.
  2. 02Target receives hundreds of legitimate-looking reset emails per minute.
Recommendation

Add a per-target-account rate limit (e.g. 5 reset requests per hour per email), independent of IP. Return identical responses for known and unknown accounts to prevent enumeration as a side-effect of the limit.

INFOUSR-009ApplicationOpen
app/auth/login/route.ts

Inconsistent error messages enable user enumeration on login

Summary

The login endpoint returns subtly different responses for unknown email versus wrong password. While the textual message is identical, response timing differs by ~80ms in a way that allows user enumeration.

Impact

Attackers can enumerate valid user accounts. Useful as a precursor to targeted phishing or credential stuffing campaigns.

Exploit path
  1. 01Submit known-bad credentials for a candidate email and measure response time.
  2. 02Compare to a known-bad credential for a definitely-unregistered email.
  3. 03The timing delta indicates whether the email exists.
Recommendation

Use a constant-time path that performs a dummy bcrypt compare for unknown users. Audit all auth-adjacent endpoints for similar timing leaks.

INFOUSR-010BackendFixed
lib/error-handler.ts

Verbose error stack traces in API responses

Summary

Unhandled errors in production return a JSON body containing the full stack trace, including absolute file paths. Useful intel for an attacker mapping the system.

Impact

Information disclosure. Reveals deployment paths, library versions, and internal module names.

Exploit path
  1. 01Send malformed input to any API endpoint that doesn't have its own error handler.
  2. 02Receive a 500 response containing the full stack trace.
Recommendation

Strip stack traces in production responses. Log them server-side with a correlation ID returned to the client for support.

End of report

This sample shows the format and depth of an Unlisted Security engagement report. On a real engagement, the exact set of findings, surfaces, and reproductions will of course depend on your system. We'd like to find them with you.

Engagements opening Q1–Q2

Find what others miss.
Before attackers do.

Tell us what you're building. We'll come back with a focused scope, a fixed quote, and a sample of the kinds of risks we expect to find on a system like yours.