# Agentic SAST — aunovatechnologiesltd

## Summary
Three findings across dev config, DSR logic, and Nostr relay handling. Production surface (fully static Cloudflare Pages) blocks server-side exploitation. Highest real risk is the dev-server config: allowedHosts:true on an internet-exposed cloudflared tunnel exposes Vite's /@fs/ endpoint, creating a credible path to steal the Cloudflare deploy token and push a malicious JS bundle to all visitors. The other two findings are standalone medium/low severity with no viable chains between them.

## Scan Metrics

- Scan ID: 2026-07-27T08:26:14Z__aunovatechnologiesltd
- Module: aunovatechnologiesltd
- Start: 2026-07-27T08:26:14Z
- End: 2026-07-27T08:49:35Z
- Duration (sec): 1401
- Files in scope: 51
- Files analyzed (unique): 46
- Coverage: 90.2%
- Chunks: 28 (risk=4, catch-all=8, specialist=16)
- Tokens (prompt): 498781
- Tokens (completion): 176118
- Tokens (total): 674899

- Folders scanned: 12
### Tokens by Phase

_Prompt = fresh + cache-write (billable). Cache-read shown separately, NOT included in totals._

| Phase | Calls | Prompt | Completion | Total | % | Cache-read (excl.) |
|---|---:|---:|---:|---:|---:|---:|
| s4-deepdive | 28 | 266,581 | 118,474 | 385,055 | 57.1 | 49,496 |
| s6-verify | 10 | 151,756 | 31,130 | 182,886 | 27.1 | 1,255,146 |
| s1-preprocess | 1 | 45,652 | 6,447 | 52,099 | 7.7 | 277,385 |
| s2-threatmodel | 1 | 5,991 | 13,958 | 19,949 | 3.0 | 0 |
| unlabeled | 1 | 11,668 | 4 | 11,672 | 1.7 | 18,179 |
| s3-decompose | 1 | 7,423 | 3,910 | 11,333 | 1.7 | 0 |
| s1-autoexclude | 1 | 5,637 | 1,964 | 7,601 | 1.1 | 0 |
| s7-dedup | 1 | 4,073 | 231 | 4,304 | 0.6 | 0 |

### Language LOC Coverage

| Language | LOC in scope | LOC scanned | Coverage % |
|---|---:|---:|---:|
| javascript | 24 | 24 | 100.0 |
| other | 2266 | 1777 | 78.4 |
| typescript | 2423 | 2423 | 100.0 |

## Threat Model

### System context

Fully static Astro 7 site deployed to Cloudflare Pages — zero server runtime, no API routes, no database. Marketing placeholder for Aunova Technologies / Greenblocks Digital Building Passport (Dubai premium real estate). Two interactive surfaces: (1) ContactForm React island — collects email/company/message, sanitizes, composes NIP-17 gift-wrapped Nostr DM, publishes to hardcoded relays entirely in-browser; (2) GDPR DSR page — HTML form with mailto: action, zero backend. Deploy is manual via wrangler CLI with a Cloudflare API token. All crypto is client-side (NIP-44 via nostr-tools). NIP-42 relay auth and gift-wrap use throwaway ephemeral keys per connection. CSP in public/_headers carries unsafe-inline for both script-src and style-src.

### Assets

| Asset | Sensitivity | Description |
|---|---|---|
| Contact form PII | medium | Submitter email, company name, and message text — briefly plaintext in browser DOM before NIP-44 encryption; relayed encrypted over Nostr |
| Site integrity | high | JS/HTML/CSS bundle served from Cloudflare CDN to all visitors — compromise enables arbitrary code execution in every visitor's browser |
| Cloudflare deploy credentials | critical | CLOUDFLARE_API_TOKEN (or wrangler session) used in manual deploy — sole gate controlling what JS/HTML is published to production aunova.ae |
| Recipient Nostr private key | high | Site owner's long-term Nostr private key used to decrypt gift-wrapped contact DMs — held in owner's client, not in codebase; loss = loss of all past and future contact messages |
| Operator email address | low | Site owner email embedded in plaintext mailto: href in the DSR page — visible to any visitor or scraper |
| Nostr message metadata | low | Ephemeral sender pubkey, hardcoded recipient pubkey, relay timestamps, and sender IP — visible to relay operators despite NIP-44 content encryption |
| GDPR DSR process integrity | medium | Authenticity of data-subject requests — operator acts on deletions/exports based on unverified identity claims from the form |

### Trust boundaries

- **src/components/ContactForm.tsx::handleSubmit** — unauth browser → client-side NIP-17 DM pipeline (WebSocket to external Nostr relays) → Contact form PII, Site integrity, Nostr message metadata
- **src/pages/privacy/data-request.astro::(form action=mailto:)** — unauth browser → operator MUA (no server processing) → Operator email address, GDPR DSR process integrity
- **npm registry → client bundle (build time)** — third-party supply chain packages → bundled and deployed JS served to all visitors → Site integrity, Contact form PII
- **wrangler pages deploy → Cloudflare Pages CDN** — developer machine / build environment → Cloudflare CDN origin (token-authenticated) → Site integrity, Cloudflare deploy credentials
- **Nostr relay WebSocket → client** — external relay operator → browser WebSocket receive handler → Nostr message metadata, Site integrity

### Ranked threats

| ID | Threat | Actor | Surface | Asset | Impact | Likelihood | Controls |
|---|---|---|---|---|---|---|---|
| T1 | Leaked or stolen Cloudflare API token lets attacker deploy a malicious JS bundle to aunova.ae, achieving persistent arbitrary code execution in all visitors' browsers | local_admin | wrangler pages deploy → Cloudflare Pages CDN | Site integrity | critical | rare | Manual deploy keeps a human in the loop; Cloudflare account MFA status unknown |
| T2 | Compromised npm dependency (nostr-tools, @react-three/fiber, gsap, lenis, or any transitive package) exfiltrates contact form PII in plaintext from the browser DOM before NIP-44 encryption completes | supply_chain | npm registry → client bundle (build time) | Contact form PII | high | rare | none — no SRI on npm-bundled assets, no pinned integrity hashes beyond lockfile |
| T3 | unsafe-inline in script-src and style-src eliminates browser XSS backstop, ensuring any future injection vector (SSR migration, CDN compromise, supply chain) executes without CSP interference | remote_unauth | src/components/ContactForm.tsx::handleSubmit | Site integrity | high | rare | Static-only build currently limits injection surface; unsafe-inline is required by Astro inline scripts |
| T4 | SSR adoption with user-controlled values (e.g., URL params) reaching set:html LD+JSON schema objects causes XSS because JSON.stringify does not escape </script> sequences | remote_unauth | src/components/ContactForm.tsx::handleSubmit | Site integrity | high | very_rare | Threat is currently inert — output:static build; activates only on SSR migration |
| T5 | Absent rate limiting on ContactForm allows automated flooding of the operator's hardcoded Nostr recipient pubkey, drowning legitimate contact submissions and exhausting operator attention | remote_unauth | src/components/ContactForm.tsx::handleSubmit | Contact form PII | medium | likely | none — no CAPTCHA, no client-side throttle, no backend validation; relay-side rate limits unknown |
| T6 | Fraudulent GDPR DSR with falsified identity causes operator to delete or export another person's data, because the mailto: form performs no identity verification whatsoever | remote_unauth | src/pages/privacy/data-request.astro::(form action=mailto:) | GDPR DSR process integrity | medium | possible | none |
| T7 | Nostr relay operators observe ephemeral sender pubkey, hardcoded recipient pubkey, source IP, and message timestamps, enabling traffic-analysis correlation of who contacts Aunova despite NIP-44 content encryption | adjacent_network | Nostr relay WebSocket → client | Nostr message metadata | low | almost_certain | NIP-44 encrypts message body; NIP-17 throwaway gift-wrap key partially obscures sender — metadata remains visible to relays by protocol design |
| T8 | Operator email address in plaintext mailto: href is harvested by automated scrapers, enabling sustained spam and targeted phishing against the site owner | remote_unauth | src/pages/privacy/data-request.astro::(form action=mailto:) | Operator email address | low | almost_certain | none |
| T9 | Any Nostr client can gift-wrap arbitrary messages to the hardcoded recipient pubkey, impersonating contact form submissions or mass-flooding the inbox, because NIP-17 throwaway keys provide no sender authentication | remote_unauth | Nostr relay WebSocket → client | Contact form PII | low | possible | none — sender anonymity is intentional in NIP-17; recipient cannot distinguish form vs. arbitrary Nostr senders |

### Open questions

- Does sendDM sign the kind:13 seal with a fixed site Nostr private key hardcoded in the client bundle, or a fresh throwaway per message? A fixed private key in browser JS is extractable by any visitor and enables sender impersonation.
- Is the Cloudflare account protected by MFA? Single-factor compromise = full site takeover via T1.
- Is bun.lockb committed and integrity-verified in the repo? Missing or unpinned lockfile widens the supply-chain window for T2.
- Are the hardcoded Nostr relay URLs public third-party relays or Aunova-controlled infrastructure? Third-party relays expand T7 metadata exposure and T9 spam surface.
- Is CLOUDFLARE_API_TOKEN scoped to the 'aunova' Pages project only, or account-wide? Account-wide token blast radius covers all Cloudflare properties.
- Does Codeberg enforce branch protection on main? An insider-pushed malicious commit that the operator deploys manually achieves T1 without requiring Cloudflare credentials.
- Is there any Cloudflare Bot Management, Turnstile, or WAF rule in front of the Pages deployment that would rate-limit T5 (Nostr spam)?
- Will the site add SSR routes for Greenblocks dynamic content? SSR adoption activates T4 and widens injection surface significantly.
- What is the operator's procedure for verifying GDPR DSR identity before acting? No procedural control visible in codebase, which is the only mitigation for T6.

## Verification
- Raw findings (pre-verification): 11
- True positives (verified): 3
- False positives (dropped): 7
- Verifier errors (excluded — undetermined, not confirmed clean): 0
- Duplicates collapsed (all passes): 0
- Verification precision: 27.3%

## Findings (3)

### 1. [MEDIUM] allowedHosts:true disables DNS-rebinding protection on public-tunneled dev server
**Class:** CWE-346
**CWE:** CWE-346 - https://cwe.mitre.org/data/definitions/346.html
**File:** `astro.config.mjs:13-18`
**CVSS 3.1:** **6.8** (Medium) — `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N`
**OffensivePriority:** **P3** - Internal Network / Privileged Position | *internal-network position required*
**Confidence:** 0.72 (1 run agreed)

#### Description
Vite's `allowedHosts` setting exists specifically to prevent DNS-rebinding attacks: by default it rejects requests whose Host header does not match a known safe value, blocking a remote page from making a browser-initiated request that appears to come from localhost. Setting `allowedHosts: true` removes all Host validation. The inline comment explicitly states that the dev server is intended to be reachable over cloudflared public tunnels (`.trycloudflare.com`), which means the Vite dev server is reachable from the open internet with no Host-header restriction whatsoever. Trust boundary: an internet-facing request (via the cloudflared URL) enters the Vite HTTP stack at network ingress; the Host-validation security decision is made — and skipped — at `astro.config.mjs:17`. There is no secondary guard between the public URL and Vite's internal endpoints (`/@fs/`, `/__vite_hmr`, `/@id/`, etc.).

#### Impact
Vite's Host-header origin check — its only CSRF/DNS-rebinding guard — is fully disabled. Any internet attacker who knows the active cloudflared tunnel URL (or who controls the victim's DNS resolution) can send arbitrary HTTP requests to the dev server and read source files, trigger HMR endpoints, or probe internal routes that the dev server exposes, with no credential required.

#### Exploit scenario
Developer runs `astro dev --host` with a cloudflared tunnel active. Attacker discovers or brute-forces the `*.trycloudflare.com` subdomain. Attacker GETs `https://<tunnel>.trycloudflare.com/@fs//home/developer/dev/project/src/components/ContactForm.tsx` — Vite's `@fs` virtual module serves arbitrary filesystem paths reachable from the project root. Because `allowedHosts: true` skips the Host check, the response is returned. Attacker reads private keys, Nostr nsec material, or any file on the developer's machine that Vite can reach.

#### Preconditions
- Developer must be running `astro dev` with this config active
- A cloudflared (or equivalent) public tunnel must be established — explicitly anticipated by the comment at line 15
- Attacker must know or enumerate the tunnel subdomain (trycloudflare.com subdomains are sequential/guessable)

```
    server: {
      // dev-only: allow any Host header so `astro dev --host` is reachable over
      // the LAN by IP *and* hostname/.local mDNS, plus cloudflared tunnels
      // (.trycloudflare.com). An explicit allowlist here 403s LAN hostnames.
      allowedHosts: true,
    },
```

#### How to fix
Replace `allowedHosts: true` with an explicit allowlist: `allowedHosts: ['localhost', '127.0.0.1', '.trycloudflare.com']`. This preserves LAN + tunnel reachability while keeping Vite's Host-validation guard active for all other origins. Vite 5+ also supports `allowedHosts: 'auto'` which allows only the bound address and explicitly added entries.

**Exploitability:** CVSS base calculates to ~8.8 (High band: AV:N/AC:H/PR:N/UI:N/S:C/C:H). Vite's /@fs/ handler with allowedHosts:true on a public cloudflared URL gives unauthenticated arbitrary filesystem read — including .env, wrangler.toml, or any file under project ancestors. Preconditions: dev server running, cloudflared tunnel active, attacker discovers the random *.trycloudflare.com subdomain (AC:H). Non-prod code downgrade rule applies (this is dev-only config, not production) → drop from High to MEDIUM. However this finding is the sole enabler of the high-severity chain below, which is why it ranks first.

#### Adversarial verification
**Verdict:** TRUE_POSITIVE (confidence: 7/10) — `allowedHosts: true` confirmed; comment proves cloudflared tunnel use is intentional, making Vite dev endpoints publicly reachable with no Host guard; `server.fs` unrestricted leaves project source readable via `/@fs/`

Facts gathered:

- Line 17 confirmed: `allowedHosts: true` — exact match to scanner snippet
- Comment explicitly names cloudflared tunnels as intended use at lines 14-16
- Zero `server.fs` restrictions in config — only Vite's default deny (`*.env`, certs) applies
- No `.env` files present in project root
- Static Astro site → no prod server, but this config governs `astro dev` only
- No auth layer in front of Vite dev endpoints (`/@fs/`, `/__vite_hmr`, etc.)

**Analysis:**

`allowedHosts: true` removes Host-header validation. Without a tunnel this is low-risk (localhost only). But the comment at line 14 *explicitly plans* for cloudflared exposure — that's what makes this cross the threshold. When `astro dev --host` + a tunnel is active, the Vite dev server is publicly reachable on the internet with zero Host restrictions. The `/@fs/` virtual module serves project-root-relative filesystem paths; without `server.fs.allow` narrowed, all project source is readable (components, pages, any secrets embedded in code).

Mitigations checked:
- `server.fs.deny` defaults: blocks `.env*` and certs — partial, not full
- No auth middleware, no Cloudflare Access in front of the tunnel
- trycloudflare subdomains: randomly generated (not sequential as scanner claims), raising AC from L→H
- Prod deployment (Cloudflare Pages static build) entirely unaffected

Precondition stack (all must hold simultaneously): dev server running + tunnel active + subdomain discovered. AC:H is accurate. But the comment proves the developer actively *uses* this path, so it's not hypothetical.

### 2. [MEDIUM] DSR form: no email-ownership verification enables spoofed erasure requests
**Class:** CWE-345: Insufficient Verification of Data Authenticity
**CWE:** CWE-345: Insufficient Verification of Data Authenticity - https://cwe.mitre.org/data/definitions/345.html
**File:** `src/pages/privacy/data-request.astro:47-117`
**CVSS 3.1:** **5.4** (Medium) — `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N`
**OffensivePriority:** **P3** - Internal Network / Privileged Position | *exposure unverified — no CMDB context; AV:N (network-routable; internet exposure unconfirmed)*
**Confidence:** 0.82 (1 run agreed)

#### Description
The form at line 47 posts `name`, `email`, `telephone`, `relationship`, `right[]`, and `details` to `mailto:privacy@aunova.ae` with `enctype="text/plain"`. The `email` field (line 61) is free-text under the HTML `type="email"` constraint, which only validates format client-side and is trivially bypassed. No step in this form — and no server-side component exists to add one — confirms the submitter owns the email address they declare. The operator mail thread shows the attacker's real From: header, but the data-subject identifier the operator will act on (look up records, decide what to erase/export) is the body field `email=victim@example.com`. The page states "we verify your identity before we action a request" (line 107) but this is an undocumented manual procedure: the privacy policy (privacy.md:59) gives no description of HOW verification is done, meaning there is zero technical enforcement and no documented process an auditor could audit against — violating the procedural transparency GDPR Art.12(1) requires.

#### Impact
Any anonymous visitor can submit a GDPR data-subject request (including erasure) while claiming to be any other person's email address. If the operator acts on the body-supplied email rather than cryptographically confirming the requester owns that address, a third party's data is deleted or disclosed. One request per victim; no authentication barrier for the attacker.

#### Exploit scenario
Attacker visits /privacy/data-request and fills in `name=Alice Smith`, `email=alice@target.com`, checks the `erasure` box, types a convincing `details` value, then clicks Send. Their own mail client opens pre-addressed to `privacy@aunova.ae` with the form body already composed; they send it. The operator sees a plausible DSR for `alice@target.com` arriving from an unknown From: address. Without a defined out-of-band verification step, the operator emails alice@target.com to confirm — but the attacker chose a real user's address, so the operator may proceed after receiving no objection or may misread the From: header as proof. Outcome: Alice's Greenblocks data deleted without her consent, constituting a GDPR Art.17 violation by the controller.

#### Preconditions
- Attacker knows (or guesses) the email address of a registered data subject
- Operator's manual verification procedure fails to require the data subject to reply from the claimed address or provide another proof of ownership

```
<form
  class="dsr-form"
  action="mailto:privacy@aunova.ae"
  method="post"
  enctype="text/plain"
>
  ...
  <input id="email" name="email" type="email" autocomplete="email" required />
  ...
  <button type="submit" class="dsr-submit">Send request</button>
</form>
```

#### How to fix
Add an email-confirmation step before the request reaches the operator: after form submission (or in a backend relay), send a one-time token to the declared email address and require the data subject to click it before the DSR is forwarded or logged. On the static-site architecture (no backend), this requires a small serverless function (Cloudflare Worker / Pages Function) or a third-party form service that provides email verification. Additionally, update privacy.md and data-request.astro to describe the concrete identity-verification procedure the operator follows (e.g., 'we will reply to the address you provide and require you to confirm from that address before processing') to satisfy GDPR Art.12(1) transparency.

**Exploitability:** CVSS ~4.3 (Medium band: AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L). Unauthenticated attacker submits DSR form with victim's email; operator may action erasure/export against that email with no technical verification. Single precondition (know victim email), blast radius one data record, impact depends on operator following unverified request. The 'we verify identity' claim in UI text with zero documented/technical enforcement is also a GDPR Art.12(1) compliance gap. No chain amplification possible — static site, no server-side joins to escalate. Stays MEDIUM.

#### Adversarial verification
**Verdict:** TRUE_POSITIVE (confidence: 6/10) — code confirmed, no technical identity verification exists; exploitation requires operator to fail standard GDPR verification practice, but no documented process enforces that practice

Analysis complete. Key facts:

**Confirmed:**
- Form exactly matches scanner description: `action="mailto:privacy@aunova.ae"`, `enctype="text/plain"`, email is `type="email"` (client-side only)
- Zero API routes (`src/pages/api/` doesn't exist), fully static Astro site
- No backend, no email confirmation loop, no server-side identity verification anywhere
- Privacy policy (line 59) says "exercise a right, use our privacy request form" — no description of HOW verification works
- Form says "we verify your identity" (line 107) — stated intent only, zero technical enforcement

**Mechanism:**
`mailto:` opens attacker's own mail client → email arrives at `privacy@aunova.ae` with:
- `From: attacker@evil.com` (attacker's real address — operator CAN see this)
- Body: `email=victim@target.com`

**Defence assessment:**
The only defence is the operator's undocumented manual process. Operator DOES receive the real `From:` header — a competent GDPR-trained handler would spot the mismatch and require the claimed address to confirm. BUT: no technical barrier prevents the spoofed submission, and no documented procedure exists that an auditor (or operator under time pressure) can rely on. For a DATA ACCESS request, if operator responds to the `From:` address (the legitimate flow for delivering data) they send victim data to attacker. For erasure, operator may act without reply-confirmation.

Technical path: fully open. Exploitation requires operator procedure failure, which is a real risk at a 2-person company without documented DSR handling process.

### 3. [LOW] Empty-sentinel authEventId matched by relay-crafted OK message
**Class:** CWE-20: Improper Input Validation
**CWE:** CWE-20: Improper Input Validation - https://cwe.mitre.org/data/definitions/20.html
**File:** `src/lib/sendDM.ts:117-198`
**CVSS 3.1:** **3.1** (Low) — `CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L`
**OffensivePriority:** **P3** - Internal Network / Privileged Position | *exposure unverified — no CMDB context; AV:N (network-routable; internet exposure unconfirmed)*
**Confidence:** 0.78 (1 run agreed)

#### Description
authEventId is initialised to the empty string '' on line 117 and is only written once, at line 166, when an actual AUTH challenge has been processed. Between connection-open (where the EVENT is sent on line 138) and the arrival of a real AUTH challenge, authEventId remains ''. The onmessage handler at line 176 compares the relay-supplied data[1] — cast to string without format validation — directly against authEventId. A relay that responds with the message ['OK','',false,'your auth was rejected'] will satisfy id === authEventId (both are ''), enter the authEventId branch, and execute finish(() => reject(...)) on line 181, aborting the connection and marking that relay as failed. Alternatively, ['OK','',true,''] makes the branch set authResent = true (line 178) and resend the EVENT without any auth having occurred; a subsequent genuine auth-required rejection from the relay then hits the (!isAuth && !authTried) || authResent path on line 195 and also rejects — but authTried is still false, so the original guard logic's intent (only reject after a completed auth round-trip) is violated. Trust boundary: the Nostr relay WebSocket is listed explicitly as an untrusted entry point in the trust context.

#### Impact
A malicious or compromised relay can send ['OK','',false,'reason'] before any AUTH handshake has been initiated, causing the state machine to immediately call finish→reject for that relay connection. If all five relays do this concurrently, sendDM throws and the contact form silently fails to deliver. Single-relay delivery still proceeds if the other four relays behave correctly.

#### Exploit scenario
An operator of wss://nip17.com (one of the five hardcoded relays) modifies the relay to respond immediately after receiving any EVENT message with the frame ['OK','',false,'auth-required: bad session']. The client's onmessage fires, parses data[1] as '', compares '' === authEventId (also ''), finds ok===false, and calls finish(reject). The promise for that relay rejects. Because all five relays are tried with Promise.allSettled and the nip17.com relay is the one open relay that normally succeeds for non-auth senders, if the four remaining relays require NIP-42 AUTH and the throwaway-key auth succeeds, the message is still delivered — but if the open relay is the only one accepting, the overall sendDM call throws 'Could not reach a relay' and the visitor's enquiry is silently lost.

#### Preconditions
- Attacker must operate or have compromised one of the five hardcoded relay WebSocket endpoints
- The target relay must be the sole relay that would otherwise accept the gift-wrap without AUTH, making its rejection fatal to the overall send

```
let authEventId = ""; // line 117 — empty-string sentinel
...
if (id === authEventId) { // line 176 — relay-supplied data[1] matched with no guard
  if (ok) {
    authResent = true;
    ws.send(JSON.stringify(["EVENT", event]));
  } else {
    finish(() => reject(new Error(`Auth rejected: ${reason}`))); // line 181
  }
  return;
}
```

#### How to fix
Do not use '' as the initial value of authEventId — use null or undefined, and guard the authEventId branch with a truthiness check: if (authEventId && id === authEventId). This ensures the branch is only entered after a real AUTH event has been processed and authEventId has been set to a non-empty 64-char hex event ID. Change line 117 to `let authEventId: string | null = null;` and line 176 to `if (authEventId && id === authEventId)`.

**Exploitability:** CVSS A:L anchors this to Low band. Relay-crafted OK with empty-string event ID can abort a single relay connection (DoS one delivery attempt) or trigger authResent causing a misleading UI state before ultimate rejection. Requires attacker to control or MITM a Nostr relay endpoint the browser actually connects to (AC:H, trust boundary explicitly external). Primitive is availability-only on a single non-critical message delivery. No escalation path — relay cannot read plaintext PII or inject content into the page. LOW.

#### Adversarial verification
**Verdict:** TRUE_POSITIVE (confidence: 8/10) — empty-string sentinel genuinely matchable by relay-crafted `['OK','',false,'...']`; no format guard; attacker must control one hardcoded relay endpoint; impact is limited to message-delivery DoS

Code confirmed. Key observations:

**Line 117:** `let authEventId = ""`  
**Line 166:** `authEventId = authEvt.id` (only write, inside `type === "AUTH"` branch)  
**Line 176:** `if (id === authEventId)` — `id` is `data[1] as string`, no format guard, no null-check on `authEventId`

**Trace the exploit path:**

1. `ws.onopen` → sends `["EVENT", event]` (line 138)
2. Relay immediately replies `["OK","",false,"rejected"]` — before any AUTH challenge arrives
3. `onmessage`: `type === "OK"` passes line 171 guard; `id = ""`, `authEventId = ""`  
4. `id === authEventId` → `true` → `ok === false` → `finish(() => reject(...))` at line 181
5. `authTried` never set, `authResent` never set — whole relay slot dies

**Dead-path check:** The `type === "AUTH"` branch at line 152 would set `authEventId` before the `OK` branch could misfire — but that only helps if AUTH arrives *before* the relay sends an `OK` for the empty-id. A hostile relay can reorder these at will.

**`Promise.allSettled` partial mitigation:** If ≥1 of the other 4 relays accepts, message delivered. Attack only achieves total DoS when the manipulated relay is the sole accepting relay (e.g. nip17.com open relay is compromised; the 4 auth-gated relays fail auth or are down).

**Precondition weight:** Attacker must control/compromise one of 5 hardcoded WSS endpoints — non-trivial, but these are third-party relay operators, explicitly named as untrusted in the trust boundary. No user-controlled relay injection possible (hardcoded constant, ContactForm never overrides).

**Impact ceiling:** Message delivery DoS only. Content stays E2E-encrypted (NIP-44). No confidentiality/integrity breach.

**Fix is trivial:** add `if (!authEventId) return;` before line 176, or init sentinel to `null`.

Finding is **real** — sentinel collision is confirmed in actual code, path to external entry point exists (ContactForm → sendDM → publishViaWebSocket → relay WebSocket), no upstream guard neutralises it.

## Exploit Chains

No exploit chains were identified — the findings above are independent and do not combine into a multi-step path.


## Dropped Findings

- **[UNCONFIRMED]** `src/lib/polyfills.ts:56` logic-flaw (spec-logic-bug-02) — s4 confidence 0.55 < gate 0.60
- **[FP]** `src/pages/privacy/data-request.astro:43` info-leak (chunk-04) — legally mandated GDPR/PDPL contact email, intentionally published including in llms.txt; obfuscation would violate regulatory requirements; zero exploitable security impact
- **[FP]** `src/layouts/BaseLayout.astro:50` injection (chunk-02) — fully static Astro build; all schema inputs are build-time constants with no HTTP-request-derived data; exploit requires repo access to change output mode and write new route code
- **[FP]** `public/_headers:2` other (chunk-03) — `unsafe-inline` confirmed in CSP, but scanner's exploit chain requires a DOM sink rendering relay data; ContactForm discards relay responses entirely, rendering only hardcoded strings; no such sink exists anywhere in the codebase
- **[FP]** `astro.config.mjs:13` other (spec-access-control-08) — `vite.server.allowedHosts` is dev-server-only config; production is static files on Cloudflare Pages with no Vite process running; attack surface exists only on developer workstations during local development, placing this squarely in the "tooling run only on developer's own workstation" out-of-scope bucket
- **[FP]** `src/components/ContactForm.tsx:26` logic-flaw (chunk-01) — factually accurate description, real attack vector, but impact is pure volumetric inbox-spam DoS against the operator; excluded by the "pure volumetric / rate-limit DoS — infra concern" rule; no data exfiltration, auth bypass, or code execution path exists
- **[FP]** `src/components/ContactForm.tsx:84` logic-flaw (spec-logic-bug-04) — confirmed blank-submit publishes empty DM, but impact is volumetric inbox spam on a public contact form; falls under "pure volumetric DoS — infra concern" out-of-scope rule; no data exposure, auth bypass, or code execution
- **[FP]** `src/lib/polyfills.ts:34` logic-flaw (spec-logic-bug-02) — polyfill never invoked; nostr-tools uses no AbortSignal; three.js (sole real caller) is unrelated hero canvas component


---

## Appendix: Scan Scope

### Folders scanned (12)

- `./`
- `public/`
- `src/components/`
- `src/components/hero/`
- `src/components/hero/acts/`
- `src/components/sections/`
- `src/components/ui/`
- `src/layouts/`
- `src/lib/`
- `src/pages/`
- `src/pages/privacy/`
- `src/scripts/`

### Excluded from scan (20457 files)

**Folders** (matched `exclude_dirs`):

- `node_modules/` — 19914 files
- `.git/` — 482 files
- `dist/` — 41 files
- `.astro/` — 4 files
- `banks/` — 3 files
- `.vscode/` — 2 files
- `docs/` — 1 files
- `.claude/` — 1 files

**File types** (matched `exclude_exts`):

- `*.png` — 3 files
- `*.lock` — 1 files
- `*.svg` — 1 files
- `*.ico` — 1 files
- `*.jpg` — 1 files
- `*.webp` — 1 files

**Patterns** (matched `exclude_globs`):

- `**/.gitignore` — 1 files
