The Auditware team has performed a security review on ShieldFlow's Protocol,
a fork of the Privacy Pools Core protocol in March 2026.
The scope was:
- Minor additions to the contract code made during ShieldFlows fork
from the original protocol - ShieldFlow's Website and ASP
- All referenced documentation and architectural materials were provided directly by the ShieldFlow team and reviewed as part of this engagement
Audit commit hash:
Several critical and high issues were discovered during the test, it's highly recommended to fix all of them before deploying the protocol.
As with most protocols, it is highly recommended to:
- Conduct regular and thorough audits to identify and mitigate vulnerabilities.
- Achieve the highest possible test coverage, ideally approaching 100%.
- Maintain comprehensive and up-to-date documentation.
- Participate in auditing contests and/or a bug bounty program.
- Implement on-chain monitoring for real-time threat detection and mitigation.
Moreover, we recommend to consider gradual deployment that will incentivize more community researchers to participate in securing the codebase, by applying following strategy or similar:
- Deposit cap grace period - when the protocol is live, limit the amount that daily users can deposit into the protocol.
- Use the gradually-attained TVL to allocate part of the funds to security professionals (incentivize security review to the code via bug bounty / contest-based rewards).
- Only after critical issues are cleared, end the deposit limit grace period.
Retest 31.3.2026
During March the ShieldFlow team had reviewed the report and requested a retest on all raised issues, the retest commits were:
Of the reported findings, 15 are Fixed and 3 are Partially Fixed (AW-C-03, AW-H-02, AW-L-03). No findings remain fully unaddressed. The three partials retain only defense-in-depth gaps - none of which are independently exploitable without prior privileged access.
Retest 6.4.2026
During April 2026 the fixes resulting from the updated retest had concluded ShieldFlow to have addressed the findings of the report (with the exception of L-03 which is classified as low severity).
| Finding | Repo | Component | Risk Level | Status |
|---|---|---|---|---|
| AW-C-01: Production Secret Extraction via SSRF to Path Traversal in Relayer Proxy | shieldflow-website | Relayer Proxy API | Critical | Fixed |
| AW-C-02: Complete User Fund Theft via 53-bit Entropy Truncation in Master Key Generation | shieldflow-core | SDK | Critical | Fixed |
| AW-C-03: Overly Permissive OIDC Trust - Any Branch Can Assume Deployer Role | shieldflow-asp | CI/CD | Critical | Fixed |
| AW-C-04: Deployer Role Has Account-Wide ECR/ECS Write | shieldflow-asp | AWS IAM | Critical | Fixed |
| AW-H-01: Staging Relayer Assigned Wrong IAM Task Role - Production EFS Write | shieldflow-core | ECS Task Config | High | Fixed |
| AW-H-02: Shared Execution Role Exposes All Production Secrets to Any Container | shieldflow-asp | ECS Execution Role | High | Fixed |
| AW-H-03: Protocol Bypass via Fail-Open Compliance on Unmapped ChainId | shieldflow-asp | Compliance Service | High | Fixed |
| AW-H-04: Withdrawal DoS via Unverified Entrypoint Address in ASP Config | shieldflow-asp | Admin Config | High | Fixed |
| AW-H-05: Weak Authentication Mechanism Prone to Brute Force Attacks | shieldflow-asp | Admin API | High | Fixed |
| AW-H-06: No Relay Fee Cap Enables Silent Fee Inflation Beyond Displayed Rate | shieldflow-core | Relayer Service | High | Fixed |
| AW-M-01: R-Only HKDF IKM Breaks Cross-Session Seed Determinism | shieldflow-website | Wallet Seed | Medium | Fixed |
| AW-M-02: Missing chainId and verifyingContract in EIP-712 Domain Enables Phishing Replay | shieldflow-website | Smart Contracts | Medium | Fixed |
| AW-M-03: Staging and Production Share the Same EFS Filesystem | shieldflow-asp | EFS Config | Medium | Fixed |
| AW-L-01: Nominis API Key Exposed in URL Query Parameter | shieldflow-asp | Compliance Service | Low | Fixed |
| AW-L-02: Integer Overflow in Withdrawal TVL Aggregation | shieldflow-asp | DB / SQLite | Low | Fixed |
| AW-L-03: Content Security Policy Permits Unsafe Inline Script Execution | shieldflow-website | Security Headers | Low | Partially Fixed |
| AW-L-04: Mnemonic Phrase Retained in React Component State | shieldflow-website | Wallet UI | Low | Fixed |
| AW-L-05: Dockerfiles Do Not Pass --ignore-scripts to npm/yarn Install | shieldflow-asp | Dockerfiles | Low | Fixed |
AW-C-01: Production Secret Extraction via SSRF to Path Traversal in Relayer Proxy {#aw-c-01:-production-secret-extraction-via-ssrf-to-path-traversal-in-relayer-proxy}
Severity: Critical Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that localhost and 127.0.0.1 were removed from ALLOWED_HOSTS and path validation was tightened. Note that isPrivateUrl does not yet cover all loopback variants - 169.254.169.254 (AWS metadata), IPv4-mapped IPv6, and short-form loopbacks such as 127.1 remain unblocked and should be added.
Locations:
-
shieldflow-website/src/app/api/relayer-proxy/route.ts#L24-L29
-
shieldflow-website/src/app/api/relayer-proxy/route.ts#L31-L33
Description:
The relayer proxy endpoint /api/relayer-proxy is a server-side Next.js API route deployed on Vercel that forwards browser requests to ShieldFlow's relayer services. The endpoint accepts url and path query parameters from the caller, and proxies the request to the specified destination.
During the audit, two independent input-validation flaws (Server Side Request Forgery, leading to Path Traversal) allowed any unauthenticated attacker to attain a potential complete compromise of the protocol via the following way:
-
Attackers either discover shieldflow as a highly valuable target, or even just globally scan the internet for common attack heuristics using large-scale automation.
-
Attacker sends a GET to https://shieldflow.co/api/relayer-proxy?url=http://localhost:9001&path=/health as localhost is explicitly whitelisted. Port 9001 responds with a Go HTTP 404 (Which among other services may indicate use of Vercel Node Bridge Runtime), confirming an internal service is reachable on loopback. This SSRF vulnerability is possible due to the ALLOWED_HOSTS set, that allowed for localhost addresses (assumed for local staging but dragged over to prod):
const ALLOWED_HOSTS = new Set([
'relayer.shieldflow.co',
'testnet-relayer.shieldflow.co',
'localhost',
'127.0.0.1',
]); -
The path parameter is validated with startsWith('/health'), which is bypassed using URL-encoded dot-segments: path=/health/%2e%2e/2018-06-01/runtime/invocation/next. Node.js fetch() normalises the path before sending, resolving it to /2018-06-01/runtime/invocation/next which is the Vercel Lambda "next invocation" internal API. This Path Traversal (%2e%2e resolving to ../../ i.e. backward path), is possible due to the insufficiently designed isAllowedPath function that only validates that the path starts with “health” and approves anything beyond that.
function isAllowedPath(path: string): boolean {
return ALLOWED_PATHS.some((prefix) => path.startsWith(prefix));
} -
The attacker gets back a response that contains a live RS256-signed JWT issued by oidc.vercel.com scoped to the production shieldflow-website project. If any downstream service (e.g. an AWS IAM role) trusts this OIDC issuer, the token is directly usable for authentication.
-
Each response also contains a per-invocation responseCallbackCipherKey, responseCallbackCipherIV, and responseCallbackUrl (an internal AWS TCP endpoint). With these, an attacker can encrypt a crafted HTTP response and deliver it to Vercel's internal callback, injecting arbitrary content into the response of the intercepted invocation.
-
The payload exposes Vercel deploymentId, projectId, ownerId, AWS account ID, internal TCP IPs, and all internal routing tokens - providing an attacker persistent knowledge of the production infrastructure for further lateral movement.
Effectively this means any unauthenticated attacker on the internet can exfiltrate live production credentials and internal infrastructure secrets from ShieldFlow's Vercel deployment in a single HTTP request. Moreover, only this vector was investigated before the vulnerability was mitigated, and an advanced attacker might’ve mapped more local services for extended abuse surface leading to complete compromise of the AWS infrastructure etc.
PoC used:
Confirmed extracted in production:
- Signed Vercel OIDC token
- Internal AES encryption keys
- Internal response callback URLs
Recommendations:
- Remove localhost and 127.0.0.1 from ALLOWED_HOSTS - No legitimate production use case exists for proxying browser requests to loopback. These entries should be deleted entirely, local development should use the real staging URL.
- Apply thorough input validation on sensitive paths to not allow for path traversal (e.g. ../../ type attacks, encoded payloads etc)
- Replace startsWith path validation with exact match - Change ALLOWED_PATHS.some((prefix) => path.startsWith(prefix)) to ALLOWED_PATHS.includes(path).
AW-C-02: Complete User Fund Theft via 53-bit Entropy Truncation in Master Key Generation {#aw-c-02:-complete-user-fund-theft-via-53-bit-entropy-truncation-in-master-key-generation}
Severity: Critical Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that bytesToNumber was replaced with bytesToBigInt in both crypto.ts:50–54 and account.service.ts:88–92, restoring full 256-bit key entropy. The unnecessary BigInt() wrappers were also removed since bytesToBigInt already returns bigint.
Locations:
- shieldflow-core/packages/sdk/src/crypto.ts#L50-L58
- shieldflow-core/packages/sdk/src/core/account.service.ts#L88-L97
Description:
The SDK uses bytesToNumber instead of bytesToBigInt when converting BIP32 HD private key bytes into the master nullifier and master secret seeds. JavaScript's Number type is IEEE 754 double-precision and can only represent integers exactly up to 2^53 - 1 (Number.MAX_SAFE_INTEGER). A 32-byte private key value is silently truncated to 53 bits before being fed into Poseidon, reducing effective key entropy from 256 bits to 53 bits.
This affects two code paths - crypto.ts:50-58 and account.service.ts:88-97 - meaning every account ever created through the ShieldFlow SDK is cryptographically insecure.
Attack Flow:
- An attacker monitors ShieldFlow deposit / withdrawal events publicly
- Because of the bug, every user's masterNullifier and masterSecret keys are actually only 53 bits, so he spins up cloud servers and tries all 9 quadrillion possible 53-bit values, firstly just for the masterNullifier key for cost accuracy. For each one they run it through the same hash function (poseidon([BigInt(candidate)])) your protocol uses, then compare the result against the existingNullifierHash values published on-chain.
- From the cracked accounts linked masterNullifier find the highest holder (to narrow the remaining compute costs) and use the same approach to crack the masterSecret for these accounts
Once masterNullifier and masterSecret are recovered, the attacker generates a valid ZK withdrawal proof and drains the account privately and irreversibly (using the same SDK). The theft appears as a legitimate withdrawal with no on-chain evidence of compromise.
Recommendations:
- Replace bytesToNumber with bytesToBigInt (from viem) in both crypto.ts and account.service.ts. This is a one-word change per file - no BigInt() wrapping needed as bytesToBigInt already returns bigint.
AW-C-03: Overly Permissive OIDC Trust - Any Branch Can Assume Deployer Role {#aw-c-03:-overly-permissive-oidc-trust---any-branch-can-assume-deployer-role}
Severity: Critical Status: Fixed
Retest:
April 6 fixed: During the retest, it was verified that push-production in asp.yml was added in commit 2b7600b , and the ShieldFlow team manually confirmed via AWS CLI that the OIDC trust policy contains no wildcards, with roles locked to refs/heads/main.
March 31 partially fixed: It was found that the primary risk - production secrets stored in the repository and unrestricted CI/CD access to production - has been fully remediated as secrets are now injected at runtime from AWS Secrets Manager and per-environment task roles are in place, removing the original critical attack path where any user with read access to the repository could extract production credentials.
The remaining gaps are defense-in-depth hardening that reduce exposure in the event of a compromised developer account, not independent critical vulnerabilities. The OIDC trust policy still uses a :* wildcard in the sub condition for both repos, meaning any branch - not just main - can assume the shieldflow-github-deployer role. Additionally, the push-production job in asp.yml is missing the environment: production gate, so an image can be pushed to production ECR without reviewer approval (only the deploy-production job requires it). Both issues require repository write access to exploit. The recommended hardening steps are to lock the OIDC sub condition to ref:refs/heads/main and to add environment: production to the push-production job.
Locations:
- shieldflow-asp/.github/workflows/asp.yml#L62-L63
- shieldflow-core/.github/workflows/relayer.yml#L75-L76
- AWS Architecture Reference (provided) - L407
Description:
GitHub Actions CI/CD for shieldflow-asp and shieldflow-core uses OIDC federation to assume the shieldflow-github-deployer IAM role (arn:aws:iam::708325790837:role/shieldflow-github-deployer). The IAM trust policy uses a StringLike condition: repo:shieldflow-dev/shieldflow-asp:* and repo:shieldflow-dev/shieldflow-core:*. The trailing wildcard :* matches any branch, tag, or ref - not only protected branches.
Although the existing deployment workflows restrict triggers to main and dev, the IAM trust condition is evaluated independently of workflow-level branch filters. Any repository contributor can:
- Push a new workflow file to any feature branch with permissions: id-token: write and role-to-assume pointing at the deployer role ARN
- GitHub Actions issues an OIDC token for that branch, the trust condition matches, and the deployer role is assumed with no merge requirement, no approval gate, and no code review required for production deployment.
An attacker who has assumed the deployer role can push a backdoored container image to any ECR repository in the account, register a malicious task definition with arbitrary injected environment variables, and force any ECS service in the account to redeploy with the malicious image.
Recommendations:
- Restrict the IAM trust condition to the protected branch only - replace :* with :ref:refs/heads/main in the trust policy for both shieldflow-asp and shieldflow-core.
- Scope all ECR and ECS IAM permissions to specific resource ARNs, removing Resource: "*" from the deployer policy. Create separate deployer roles per service - shieldflow-asp-deployer and shieldflow-relayer-deployer - each scoped to their respective ECR repositories and ECS task families only.
- Enable ECR image signing and enforce signature verification at ECS task launch to prevent deployment of unverified images.
- Require deployment approval gates via GitHub environment protection rules on the production environment.
- Enable GitHub environment protection rules on the production environment requiring reviewer approval before any job that requests id-token: write can execute
AW-C-04: Deployer Role Has Account-Wide ECR/ECS Write {#aw-c-04:-deployer-role-has-account-wide-ecr/ecs-write}
Severity: Critical Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. The deployer policy was verified live against AWS in March 2026, with the help of the customer running the commands from their authenticated AWS cli on staging and production:
- aws iam get-role-policy --role-name shieldflow-github-deployer-production --policy-name deploy-access
which returned a scoped policy where:
- ECR push/pull is restricted to the shieldflow/asp and shieldflow/relayer repositories only
- ECS service operations are scoped to the asp-production and relayer-production services and the shieldflow clusters
- iam:PassRole is restricted to four named production role ARNs.
The original shared shieldflow-github-deployer role has also been split into separate shieldflow-github-deployer-staging and shieldflow-github-deployer-production roles, reducing blast radius further.
One minor observation: ecs:DeregisterTaskDefinition also remains Resource: "*" and could be scoped to specific task definition ARNs; however, deregistering a task definition does not affect running services and the residual risk is low and the finding is considered fixed.
Locations:
- shieldflow-asp/.github/workflows/asp.yml#L75
- AWS Architecture Reference (provided) - L419
Description:
The shieldflow-github-deployer IAM role - assumed via the OIDC trust misconfiguration in AW-C-03 - carries an attached policy granting ECR and ECS write actions scoped to Resource: "*" across account 708325790837.
An attacker who has assumed the deployer role can push a backdoored container image to any ECR repository in the account, register a malicious task definition with arbitrary injected environment variables, and force any ECS service in the account to redeploy with the malicious image. ECR image tags are mutable, overwriting :latest triggers no alert and bypasses no approval gate.
The complete attack chain from a single feature branch push (AW-C-03) to a running backdoored production container requires no human intervention and completes in under five minutes, placing both the production ASP and relayer - handling live Ethereum mainnet transactions - within the blast radius.
Recommendations:
- Scope all ECR and ECS IAM permissions to specific resource ARNs, removing Resource: "*" from the deployer policy.
- Create separate deployer roles per service - shieldflow-asp-deployer and shieldflow-relayer-deployer - each scoped to their respective ECR repositories and ECS task families only.
- Enable ECR image signing and enforce signature verification at ECS task launch to prevent deployment of unverified images. Require deployment approval gates via GitHub environment protection rules on the production environment.
AW-H-01: Staging Relayer Assigned Wrong IAM Task Role - Production EFS Write {#aw-h-01:-staging-relayer-assigned-wrong-iam-task-role---production-efs-write}
Severity: High Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that relayer-task-definition.json now sets taskRoleArn to shieldflow-relayer-task-staging, and relayer-task-definition-prod.json sets it to shieldflow-relayer-task-production, correctly isolating each environment’s EFS access. ASP task definitions are similarly split across shieldflow-asp-task-staging and shieldflow-asp-task-production. All four task definitions also reference dedicated per-environment execution roles.
Locations:
- shieldflow-core/ops/aws/relayer-task-definition.json#L8
- AWS Architecture Reference (provided) - L269-L271
Description:
The relayer-staging ECS task in live AWS is running with shieldflow-asp-task as its IAM task role - the ASP service's role, not the relayer's own. The repository task definition already specifies the correct role (shieldflow-relayer-task at line 8), confirming this is a deployment drift issue in live infrastructure rather than a code bug.
The shieldflow-asp-task role grants elasticfilesystem:ClientWrite to EFS filesystem fs-077171ae1a2ff75ca without restriction to an access point, giving the staging relayer full write access to the production ASP data volume at /asp-data-production.
The staging relayer is internet-facing at testnet-relayer.shieldflow.co. any code-execution vulnerability in the staging relayer grants write access to asp.sqlite, the production ASP database. Corrupting this database causes the ASP to publish attacker-controlled Merkle roots on-chain via updateRoot(), invalidating all user inclusion proofs and blocking protocol-wide withdrawals on Ethereum mainnet.
This makes the staging relayer a one-hop path to permanent disruption of live protocol state.
Recommendations:
- Re-register the relayer-staging task definition with taskRoleArn: arn:aws:iam::708325790837:role/shieldflow-relayer-task and force a service update to terminate the currently misconfigured tasks.
- Scope EFS IAM policies to specific access point ARNs - staging containers must have no access to the production EFS mount.
AW-H-02: Shared Execution Role Exposes All Production Secrets to Any Container {#aw-h-02:-shared-execution-role-exposes-all-production-secrets-to-any-container}
Severity: High Status: Fixed
Retest:
April 6 fixed: Finding was set to fixed after receiving confirmations from the client, that had ran AWS checks on top of the code checks we had access to, disproving the partial fix.
Fom ShieldFlow: “All four roles are scoped per-service per-environment (shieldflow/asp/production/*, etc.). Each has exactly one inline policy (secrets-access) + one managed policy (AmazonECSTaskExecutionRolePolicy). Old shared role deleted.”
March 31 partially fixed: It was verified that the shared execution role was correctly split into four dedicated per-environment roles:
- shieldflow-ecs-execution-asp-staging,
- shieldflow-ecs-execution-asp-production,
- shieldflow-ecs-execution-relayer-staging,
- shieldflow-ecs-execution-relayer-production
However, the execution role IAM policy still grants secretsmanager:GetSecretValue on arn:aws:secretsmanager:eu-central-1:708325790837:secret:shieldflow/* - meaning each role can read secrets belonging to other services. Each role’s policy should be scoped to its own path (e.g. shieldflow/asp/staging/* only). Additionally, POSTMAN_PRIVATE_KEY and RELAYER_SIGNER_PRIVATE_KEY remain in Secrets Manager; migration to KMS has not been implemented.
Locations:
-
shieldflow-core/ops/aws/relayer-task-During the audit performed in March 2026, it was found that the finding was partially fixed. The primary risk - a single shared execution role granting all services access to all environment secrets - has been fully remediated. It was verified that the shared role was correctly split into four dedicated per-environment roles: shieldflow-ecs-execution-asp-staging, shieldflow-ecs-execution-asp-production, shieldflow-ecs-execution-relayer-staging, and shieldflow-ecs-execution-relayer-production. This eliminates direct cross-service privilege escalation via a compromised execution role.
-
The remaining items are defense-in-depth hardening. The IAM policy attached to each role still grants secretsmanager:GetSecretValue on a shieldflow/* path, meaning a compromised execution role could still read secrets belonging to other services. Scoping each policy to its own prefix (e.g. shieldflow/asp/staging/*) would further reduce the blast radius. Migration of POSTMAN_PRIVATE_KEY and RELAYER_SIGNER_PRIVATE_KEY from Secrets Manager to KMS has not been implemented and remains a hardening recommendation.mainnet withdrawals on behalf of any user, and POSTMAN_PRIVATE_KEY, enabling attacker-controlled updateRoot() calls that forge or block inclusion proofs protocol-wide.
This compounds every other finding: any code-execution path in AW-C-03, AW-C-04, or AW-H-01 may escalate to full hot wallet compromise with no additional steps.
Recommendations:
- Create separate IAM execution roles per service - shieldflow-asp-execution and shieldflow-relayer-execution - and scope each role's secretsmanager:GetSecretValue to the specific secret ARNs that service requires, removing the shieldflow/* wildcard.
- Staging roles must have no access to production secrets.
- Migrate RELAYER_SIGNER_PRIVATE_KEY and POSTMAN_PRIVATE_KEY to AWS KMS asymmetric signing keys or a hardware signing service to eliminate plaintext key exposure via the ECS metadata endpoint entirely.
AW-H-03: Protocol Bypass via Fail-Open Compliance on Unmapped ChainId {#aw-h-03:-protocol-bypass-via-fail-open-compliance-on-unmapped-chainid}
Severity: High Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that both code paths now fail closed - startAsyncScreening (compliance.ts line 165) and the pollPendingScreenings retry path (compliance.ts line 228) both reject deposits for unmapped chain IDs. The hardcoded CHAIN_ID_TO_NOMINIS table was replaced with a config-driven mapping enforced by Zod schema at startup.
Locations:
- shieldflow-asp/src/services/compliance.ts#L160-L165
- shieldflow-asp/src/services/compliance.ts#L19-L30
- shieldflow-asp/src/services/compliance.ts#L230-L235
Description:
During the audit, it was found that ShieldFlow's compliance service screens every deposit through the Nominis API before approving it into the protocol. The service maps each supported chain ID to its corresponding Nominis chain identifier via a hardcoded lookup table, CHAIN_ID_TO_NOMINIS. At the time of the audit, only Ethereum mainnet (chain ID 1) and Sepolia testnet (chain ID 11155111) are mapped.
When a deposit arrives from a chain ID absent from this table, the code does not reject it - it approves it unconditionally, adding it to the ASP Merkle tree without any compliance screening. A missing chain mapping is treated as a reason to skip screening rather than a reason to block.
const chain = CHAIN_ID_TO_NOMINIS[chainId];
if (!chain) {
console.error(`[Compliance] No Nominis chain mapping for chainId ${chainId}, auto-approving`);
await updateDepositStatus(chainId, poolScope, label, 'APPROVED');
await addLeaf(chainId, label);
return;
}
// pollPendingScreenings — retry path (L230-L235)
const chain = CHAIN_ID_TO_NOMINIS[req.chainId];
if (!chain) {
await this.approveDeposit(req);
continue;
}
The vulnerability exists in two independent code paths. Patching startAsyncScreening alone is insufficient - any deposit queued as PENDING will still be auto-approved by pollPendingScreenings, which contains the same fail-open branch.
Any deposit on an unmapped chain bypasses Nominis entirely and is admitted to the ASP Merkle tree without screening, exposing ShieldFlow operators to regulatory liability under OFAC with no audit trail.
Recommendations:
- Change the missing-chain branch in startAsyncScreening to call updateDepositStatus('REJECTED') rather than APPROVED - fail closed, not open.
- Apply the same fail-closed fix to the retry path in pollPendingScreenings(L230-L235); both code paths must be patched simultaneously.
AW-H-04: Withdrawal DoS via Unverified Entrypoint Address in ASP Config {#aw-h-04:-withdrawal-dos-via-unverified-entrypoint-address-in-asp-config}
Severity: High Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that entrypointAddress was removed from config.json and hardcoded in contracts.ts as audited constants, with postman.ts updated to call getEntrypointAddress(chainId), fully implementing the recommendation.
Locations:
- shieldflow-asp/src/config/index.ts#L19
- shieldflow-asp/src/services/postman.ts#L157
- shieldflow-asp/src/services/postman.ts#L217
Description:
During the audit, it was found that the Entrypoint contract address is read from config.json at startup and used by the Postman service without any on-chain verification. When posting ASP root updates, Postman calls updateRoot() on whatever address appears in this.chainConfig.entrypointAddress - it never confirms this is the legitimate deployed contract.
Attack Flow:
- An attacker gains write access to config.json via the CI/CD paths identified in AW-C-03 and AW-C-04
- The legitimate Entrypoint address is replaced with an attacker-controlled contract
- All subsequent ASP root updates are posted to the fake contract - the real Entrypoint stops receiving updates and its root goes stale.
- Users submit withdrawal proofs built against the current root, but the real on-chain Entrypoint holds a stale one - every transaction reverts with IncorrectASPRoot. Normal withdrawals are completely blocked.
- Users who cannot wait are forced into ragequit - an emergency exit that exposes deposit amounts and timing on-chain, permanently destroying their privacy guarantee.
Recommendations:
- Hardcode contract addresses - remove entrypointAddress from config.json and embed in source as audited constants.
AW-H-05: Weak Authentication Mechanism Prone to Brute Force Attacks {#aw-h-05:-weak-authentication-mechanism-prone-to-brute-force-attacks}
Severity: High Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that the vulnerable admin HTTP endpoint was removed and replaced with a 410 Gone response, with admin operations moved to a CLI tool accessible only via AWS SSM Session Manager.
Locations:
Description:
The ASP admin API authenticates requests using a static Bearer token compared with plain string equality against the ADMIN_API_KEY environment variable:
const provided = req.headers['authorization']?.replace('Bearer ', '');
if (!provided || provided !== adminKey) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
An attacker who knows the endpoint path faces no meaningful resistance:
- Every request returns a clean binary response - 401 for a wrong token, 200 for the correct one - making it a direct enumeration oracle.
- No rate limiting, lockout, or failed attempt tracking is applied specifically to admin routes.
- The only protection in place is a global limiter of 100 requests per minute per IP, trivially bypassed by distributing requests across multiple source Addresses.
An attacker can probe candidate tokens at scale with nothing on the server side to detect or interrupt the process. Once the correct key is recovered, they can call revoke-label - removing legitimate depositors from the ASP Merkle tree and locking them out of the protocol entirely.
Recommendations:
- Replace !== with crypto.timingSafeEqual() at line 289 to prevent timing-based enumeration.
- Add a dedicated rate limiter on all admin routes (max 5 req/min per IP), independent of the global limiter, with lockout after repeated failures.
AW-H-06: No Relay Fee Cap Enables Silent Fee Inflation Beyond Displayed Rate {#aw-h-06:-no-relay-fee-cap-enables-silent-fee-inflation-beyond-displayed-rate}
Severity: High Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that all three recommendations were implemented: validateWithdrawal() now rejects any relayFeeBPS above assetConfig.max_fee_bps (privacyPoolRelayer.service.ts:287–293), the client validates the quoted fee against the on-chain maximum and verifies feeRecipient before proof generation (useWithdraw.ts:229–255), and the UI displays the fee recipient address for user confirmation (WithdrawalDetailsStep.tsx:234–238).
Locations:
- shieldflow-core/src/services/privacyPoolRelayer.service.ts#L320-L330
- shieldflow-core/src/handlers/relayer/quote.ts#L45-L75
- htshieldflow-website/src/providers/QuoteContext.tsx
Description:
During the audit, it was found that when a user initiates a withdrawal, the relayer displays a fee and the user proceeds expecting to pay that amount. The relayer then signs a feeCommitment encoding the actual fee it will charge. The client receives this commitment and feeds it directly into ZK proof generation without checking whether the fee inside matches what was displayed. Once the proof is generated, the fee is cryptographically locked - it cannot be changed.
The off-chain relayer has no upper bound on the fee it can commit to. The only check in validateWithdrawal() is a lower bound - the fee must be at least the current market rate - but nothing prevents the relayer from silently committing to a higher value:
if (relayFeeBPS < currentFeeBPS.feeBPS) {
throw WithdrawalValidationError.feeTooLow(...);
}
// no upper bound check exists
A malicious or compromised relayer can show the user 0.5% and sign a commitment for 1% - the on-chain maximum on mainnet. The user generates a proof committing to the inflated fee, the transaction goes through, and they are silently overcharged with no warning and no recourse.
Attack Flow:
- Malicious relayer sets relayFeeBPS to the maximum permitted value and signs the feeCommitment.
- Client receives the commitment and passes `withdrawalData` directly into ZK proof generation - no fee range check, no verification that feeRecipient matches the expected relayer address.
- Proof is generated by committing to the inflated fee and relayer-controlled recipient - these values are now immutable.
- Relayer broadcasts the transaction. The on-chain Entrypoint accepts it as long as the fee is within maxRelayFeeBPS.
- User is silently overcharged - paying up to the on-chain maximum instead of the displayed rate - with no warning and no recourse
Recommendations:
- Add a server-side cap in validateWithdrawal(): reject any relayFeeBPS above assetConfig.maxRelayFeeBPS before the proof reaches the broadcast stage.
- Add client-side validation before proof generation: fetch and assert relayFeeBPS <= the on-chain assetConfig.maxRelayFeeBPS, and verify feeRecipient matches the expected relayer address.
- Display feeRecipient and relayFeeBPS explicitly in the UI so users can confirm both before approving proof generation.
AW-M-01: R-Only HKDF IKM Breaks Cross-Session Seed Determinism {#aw-m-01:-r-only-hkdf-ikm-breaks-cross-session-seed-determinism}
Severity: Medium Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that rawSignature.slice(0, 32) was updated to rawSignature.slice(0, 64) at line 52, correctly using the full r‖s as HKDF IKM as recommended.
Locations:
Description:
Wallet seed derivation uses only the r component (first 32 bytes) of the ECDSA signature as HKDF input keying material, discarding s and v entirely:
const signingKey = rawSignature.slice(0, 32);
The application partially mitigates this by signing the EIP-712 payload twice per session and rejecting non-matching signatures (create/page.tsx:105-113, load/page.tsx:78-86). This prevents account creation with wallets that are non-deterministic within a single session. However, the check does not protect against cross-session determinism failures.
If a hardware wallet firmware update changes the k-generation algorithm - switching from one deterministic scheme to another - the new session will produce a consistent pair of signatures that pass the double-sign check, but with a different r value than the original. The derived mnemonic will be entirely different, making all existing deposits permanently inaccessible with no warning or recovery path. For wallets using MPC or AA signing, where r may be session-scoped by design, the risk is present from the outset.
Using the full r || s (64 bytes) as IKM does not eliminate the cross-session risk entirely, but maximises the stability surface - a firmware change that alters k would need to change both r and s simultaneously to produce a divergent seed.
Recommendations:
- Change rawSignature.slice(0, 32) to rawSignature.slice(0, 64) at walletSeed.ts:52 to include both r and s as HKDF IKM, consuming the full available signature entropy.
AW-M-02: Missing chainId and verifyingContract in EIP-712 Domain Enables Phishing Replay {#aw-m-02:-missing-chainid-and-verifyingcontract-in-eip-712-domain-enables-phishing-replay}
Severity: Medium Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that chainId and verifyingContract are now added to the domain object and passed as parameters into buildSeedDerivationTypedData(), and all call sites correctly pass chainId and selectedPoolInfo.entrypointAddress.
Locations:
Description:
The EIP-712 domain in buildSeedDerivationTypedData() specifies only { name: 'ShieldFlow', version: '1' }, omitting both chainId and verifyingContract.
EIP-712 defines a domain separator to cryptographically bind signatures to a specific deployment. Without chainId, a signature obtained on Ethereum mainnet is byte-for-byte identical to one on Sepolia, Arbitrum, or any other EVM chain. Without verifyingContract, any dApp can present the identical typed data.
A phishing site at any domain can request a { name: 'ShieldFlow', version: '1' } signature and derive the exact same mnemonic as the legitimate app. Combined with AW-M-01, this creates a complete fund-theft chain: phish the user, obtain their ECDSA signature, slice r, derive the mnemonic, and drain all deposits without any on-chain interaction from the victim.
Recommendations:
- Add chainId and verifyingContract to the domain object: { name: 'ShieldFlow', version: '1', chainId: chainId, verifyingContract: entrypointAddress }. Pass these values into buildSeedDerivationTypedData() as parameters.
- Note: this changes the domain separator, so existing users will derive a different mnemonic - include a migration path and user notification before deploying.
AW-M-03: Staging and Production Share the Same EFS Filesystem {#aw-m-03:-staging-and-production-share-the-same-efs-filesystem}
Severity: Medium Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that the staging task definition now references a dedicated filesystem (fs-09996c18c7a6633d6), entirely separate from the production filesystem (fs-077171ae1a2ff75ca), eliminating the shared blast radius between environments.
Locations:
- shieldflow-asp/ops/aws/asp-task-definition.json#L69
- shieldflow-asp/ops/aws/asp-task-definition-prod.json#L69
Description:
Both the staging and production ASP task definitions reference the same EFS filesystem (fs-077171ae1a2ff75ca). While access point IDs differ per environment, both containers mount to the same underlying filesystem with IAM-enforced path isolation:
// asp-task-definition.json (staging)
"fileSystemId": "fs-077171ae1a2ff75ca",
"accessPointId": "fsap-0d5420f6b2e0bab92"
// asp-task-definition-prod.json (production)
"fileSystemId": "fs-077171ae1a2ff75ca",
"accessPointId": "fsap-02a57e8bc362eb769"
The entire isolation boundary between environments relies solely on IAM access point policies being correctly configured. Any IAM misconfiguration, permission escalation, or CI/CD pipeline compromise grants cross-environment data access. Staging workloads - with weaker security controls and greater internet-facing exposure - share a blast radius with the production ASP database. A staging compromise that reaches the production EFS volume requires no additional privilege escalation beyond the shared filesystem boundary.
Recommendations:
- Provision a separate EFS filesystem exclusively for staging. Update the staging task definition to reference a dedicated filesystem with its own access points and IAM policies.
- This eliminates the shared blast radius between environments and ensures a staging incident cannot directly affect production data.
AW-L-01: Nominis API Key Exposed in URL Query Parameter {#aw-l-01:-nominis-api-key-exposed-in-url-query-parameter}
Severity: Low Status: Fixed
Retest:
During the audit performed in March 2026, it was found that the finding was fixed. The primary risk - the Nominis api_key being transmitted as a URL query parameter and therefore written to server access logs - has been remediated. It was verified that pollResult now calls the endpoint without any api_key in the URL. The poll endpoint is designed to require only the requestId, which is a server-generated UUID that acts as a capability token - no additional authentication is needed or expected. The startScreening request correctly sends the api_key in the POST body, which is not logged. The only remaining item is operational: the original key that appeared in historical access logs should be rotated as a precaution.
Locations:
Description:
The Nominis compliance API is queried with the API key appended as a URL query parameter: const response During the audit performed in March 2026, it was found that the finding was partially fixed. The primary risk - the Nominis api_key being transmitted as a URL query parameter and therefore written to server access logs - has been remediated for the polling path. It was verified that pollResult now calls the endpoint without any api_key in the URL. The initial screen() request correctly sends the key in the request body, which is not logged. The remaining item is operational hygiene: it should be confirmed whether the Nominis poll endpoint requires authentication, and if so the key should be passed in an X-API-Key header. As the original key may already appear in historical access logs, a rotation is recommended as a precaution.alls. Its exposure allows unauthorized parties to consume API quota by making arbitrary requests, or to retrieve screening results for any known request ID.
Recommendations:
- Move the API key from the URL query parameter to an X-API-Key or Authorization request header in the pollResult method. Headers are not captured in standard HTTP access logs and are not forwarded in Referer headers. Rotate the current NOMINIS_API_KEY following this change.
AW-L-02: Integer Overflow in Withdrawal TVL Aggregation {#aw-l-02:-integer-overflow-in-withdrawal-tvl-aggregation}
Severity: Low Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that SUM(CAST(value AS INTEGER)) was replaced by fetching raw values and aggregating in JavaScript using BigInt, eliminating the overflow entirely.
Locations:
Description:
During the audit, getWithdrawalAggregates was found to aggregate cumulative withdrawal values using SUM(CAST(value AS INTEGER)):
SELECT COUNT(*) as count, COALESCE(SUM(CAST(value AS INTEGER)), 0) as total_value
FROM withdrawals WHERE chain_id = ? AND pool_scope = ?
SQLite's INTEGER type is a signed 64-bit value with a maximum of 2^63 - 1 (approximately 9.2 ETH in wei). Once cumulative withdrawals exceed this threshold, the SUM silently overflows and wraps to a negative value. The corrupted result is then passed directly to BigInt() in routes/index.ts:74, causing totalInPoolValue to misrepresent the true pool balance to API consumers and the application UI - with no error thrown.
Recommendations:
-
Replace the COALESCE(SUM(CAST(value AS INTEGER)), 0) expression with CAST(SUM(CAST(value AS INTEGER)) AS TEXT) to return the aggregated value as a string, then parse it in JavaScript via BigInt(row.total_value), eliminating any SQLite integer boundary.
-
Alternatively, pass { bigint: true } to the db.get() call:
const row = await db.get(
`SELECT COUNT(*) as count, COALESCE(SUM(CAST(value AS INTEGER)), 0) as total_value
FROM withdrawals WHERE chain_id = ? AND pool_scope = ?`,
[chainId, poolScope],
{ bigint: true }
); -
In either case, update sqlite.ts:454 to consume the BigInt value directly rather than calling .toString() on a potentially already-corrupted Number.
AW-L-03: Content Security Policy Permits Unsafe Inline Script Execution {#aw-l-03:-content-security-policy-permits-unsafe-inline-script-execution}
Severity: Low Status: Partially Fixed
Retest:
During the audit performed in March 2026, it was found that the finding was partially fixed. The primary vector - blanket script execution via unsafe-inline in script-src - has been fully closed. It was verified that script-src now uses a per-request nonce with strict-dynamic (middleware.ts line 20), meaning only nonce-tagged scripts can execute.
The remaining gap is a known dependency constraint, not an unmitigated vulnerability. The unsafe-inline directive is retained in style-src (middleware.ts line 21) because RainbowKit and next-themes inject inline styles that cannot be nonce-tagged. This is substantially less severe than the original finding: CSS-based data exfiltration requires an existing injection point and is not achievable via style injection alone when script execution is fully locked down. Removing unsafe-inline from style-src would require upstream library changes and is a hardening recommendation.
Locations:
Description:
During the audit, the Content Security Policy header was found to include unsafe-inline in both script-src and style-src directives:
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https:",
"style-src 'self' 'unsafe-inline'",
'unsafe-inline' in script-src negates the primary XSS protection CSP provides - any injected inline script via a DOM injection vulnerability, a compromised third-party resource, or a prototype pollution chain executes without CSP intervention. unsafe-inline in style-src additionally enables CSS-based data exfiltration attacks.
The developers acknowledge this in a code comment (middleware.ts:15-16):
// 'strict-dynamic' would be ideal but requires nonce support in Next.js middleware.
The risk is limited to scenarios where an XSS injection vector also exists, but CSP then provides no defence in depth.
Recommendations:
- Replace unsafe-inline in script-src with a per-request nonce-based CSP. Next.js supports nonce injection via middleware - generate a cryptographically random nonce per request and inject it into the CSP header and relevant script tags.
- Remove unsafe-inline from style-src and scope styles to specific hashes or nonces where inline styles are required.
AW-L-04: Mnemonic Phrase Retained in React Component State {#aw-l-04:-mnemonic-phrase-retained-in-react-component-state}
Severity: Low Status: Fixed
Retest:
During the audit performed in March 2026, it was found that the finding was fixed. It was verified that the mnemonic is stored in a useRef rather than component state, preventing exposure in the React DevTools fiber tree. The useRef is necessary for the multi-step wizard flow (display, copy, and verify steps all require access across renders). Critically, mnemonicRef.current is explicitly cleared immediately after initializeWithAccountService completes, annotated with a comment referencing this finding, and also cleared on the back button. The mnemonic does not persist beyond the account creation flow.
Locations:
Description:
During the audit, the full BIP-39 mnemonic phrase was found to be stored in React component state via useState<string[]>([]) and remains in JavaScript heap memory for the entire duration of the display and verification flow - potentially several minutes:
const [mnemonic, setMnemonic] = useState<string[]>([]);
Although setMnemonic([]) is called at line 186 after the user confirms the phrase, React state updates are asynchronous and the previous value persists in memory until garbage collected. During this window, the mnemonic is accessible to any browser extension with access to the page JavaScript context, including React DevTools, and could be captured by an XSS payload targeting the wallet creation flow.
The mnemonic is the root secret from which all account private keys are derived - its exposure results in permanent, irrecoverable account compromise.
Recommendations:
- Avoid storing the mnemonic in React state beyond the minimum scope required. Where display is necessary, derive words directly at render time from a scoped variable rather than persisting them in component state.
AW-L-05: Dockerfiles Do Not Pass --ignore-scripts to npm/yarn Install {#aw-l-05:-dockerfiles-do-not-pass---ignore-scripts-to-npm/yarn-install}
Severity: Low Status: Fixed
Retest:
During the audit performed in March 2026 it was found that the finding was fixed. It was verified that both Dockerfiles now pass --ignore-scripts to all install commands, with targeted npm rebuild calls retained for native modules.
Locations:
Description:
During the audit, both service Dockerfiles were found to install Node.js dependencies without the --ignore-scripts flag:
# shieldflow-asp/Dockerfile
RUN npm ci # L10 - build stage
npm ci --omit=dev # L28 - runtime stage
# shieldflow-core/packages/relayer/Dockerfile
RUN yarn install # L8
By default, npm and yarn execute lifecycle scripts defined in dependencies (postinstall, preinstall, etc.) at install time. A compromised or malicious transitive dependency can use these hooks to execute arbitrary code during the Docker build, exfiltrate build secrets, or inject backdoors into the built image.
Notably, the ASP Dockerfile already explicitly runs npm rebuild better-sqlite3 at line 29, demonstrating that the pattern for targeted native module compilation is already in place - making the fix straightforward to implement.
Recommendations:
- Add --ignore-scripts to all npm ci and yarn install invocations in both Dockerfiles. If specific dependencies legitimately require build scripts (e.g. native module compilation), allowlist only those packages with targeted npm rebuild calls. Pair with dependency pinning via lockfiles and periodic supply chain audits (npm audit, socket.dev).
This report was produced by Auditware for ShieldFlow based solely on the
information provided by ShieldFlow. Auditware provides no guarantees as to the accuracy
of the contents of this report and does not make any guarantees that following the advice
within will prevent security incidents or issues.
Auditware is available for questions or comments about any of the contents of this report. We can be reached at https://auditware.io/ or by email at joe@auditware.io.