{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "Agentic SAST",
          "version": "1.1.0",
          "rules": [
            {
              "id": "CWE-346",
              "name": "CWE-346"
            },
            {
              "id": "CWE-345: Insufficient Verification of Data Authenticity",
              "name": "CWE-345: Insufficient Verification of Data Authenticity",
              "shortDescription": {
                "text": "Insufficient Verification of Data Authenticity"
              }
            },
            {
              "id": "CWE-20: Improper Input Validation",
              "name": "CWE-20: Improper Input Validation",
              "shortDescription": {
                "text": "Improper Input Validation"
              }
            }
          ],
          "supportedTaxonomies": [
            {
              "guid": "b7c8d9e0-1f2a-3b4c-5d6e-7f8090a1b2c3"
            }
          ]
        }
      },
      "taxonomies": [
        {
          "guid": "b7c8d9e0-1f2a-3b4c-5d6e-7f8090a1b2c3",
          "name": "CWE",
          "organization": "MITRE",
          "shortDescription": {
            "text": "The MITRE Common Weakness Enumeration"
          },
          "informationUri": "https://cwe.mitre.org/",
          "taxa": [
            {
              "id": "CWE-20",
              "helpUri": "https://cwe.mitre.org/data/definitions/20.html",
              "name": "Improper Input Validation",
              "shortDescription": {
                "text": "Improper Input Validation"
              }
            },
            {
              "id": "CWE-345",
              "helpUri": "https://cwe.mitre.org/data/definitions/345.html",
              "name": "Insufficient Verification of Data Authenticity",
              "shortDescription": {
                "text": "Insufficient Verification of Data Authenticity"
              }
            },
            {
              "id": "CWE-346",
              "helpUri": "https://cwe.mitre.org/data/definitions/346.html"
            }
          ]
        }
      ],
      "properties": {
        "applicationId": "aunovadotae",
        "scanDegraded": false,
        "unrankedFallback": false
      },
      "results": [
        {
          "ruleId": "CWE-346",
          "level": "warning",
          "message": {
            "text": "allowedHosts:true disables DNS-rebinding protection on public-tunneled dev server  [CVSS 6.8: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N]"
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "astro.config.mjs"
                },
                "region": {
                  "startLine": 13
                }
              }
            }
          ],
          "properties": {
            "severity": "medium",
            "security-severity": "6.8",
            "cvssRating": "Medium",
            "category": "CWE-346",
            "cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N",
            "cwe": "CWE-346",
            "cweId": "CWE-346",
            "cvssScore": 6.8,
            "offensivePriority": "P3",
            "offensivePriorityLabel": "Internal Network / Privileged Position",
            "offensivePriorityReason": "Internal Network / Privileged Position | *internal-network position required*",
            "confidence": 0.72,
            "description": "#### Description\nVite'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 \u2014 and skipped \u2014 at `astro.config.mjs:17`. There is no secondary guard between the public URL and Vite's internal endpoints (`/@fs/`, `/__vite_hmr`, `/@id/`, etc.).\n\n#### Impact\nVite's Host-header origin check \u2014 its only CSRF/DNS-rebinding guard \u2014 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.\n\n#### Exploit scenario\nDeveloper 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` \u2014 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.\n\n#### Preconditions\n- Developer must be running `astro dev` with this config active\n- A cloudflared (or equivalent) public tunnel must be established \u2014 explicitly anticipated by the comment at line 15\n- Attacker must know or enumerate the tunnel subdomain (trycloudflare.com subdomains are sequential/guessable)\n\n```\n    server: {\n      // dev-only: allow any Host header so `astro dev --host` is reachable over\n      // the LAN by IP *and* hostname/.local mDNS, plus cloudflared tunnels\n      // (.trycloudflare.com). An explicit allowlist here 403s LAN hostnames.\n      allowedHosts: true,\n    },\n```\n\n#### How to fix\nReplace `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.\n\n**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 \u2014 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) \u2192 drop from High to MEDIUM. However this finding is the sole enabler of the high-severity chain below, which is why it ranks first.\n\n#### Adversarial verification\n**Verdict:** TRUE_POSITIVE (confidence: 7/10) \u2014 `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/`\n\nFacts gathered:\n\n- Line 17 confirmed: `allowedHosts: true` \u2014 exact match to scanner snippet\n- Comment explicitly names cloudflared tunnels as intended use at lines 14-16\n- Zero `server.fs` restrictions in config \u2014 only Vite's default\u2026"
          },
          "taxa": [
            {
              "toolComponent": {
                "name": "CWE",
                "guid": "b7c8d9e0-1f2a-3b4c-5d6e-7f8090a1b2c3"
              },
              "id": "CWE-346"
            }
          ],
          "rank": 68.0
        },
        {
          "ruleId": "CWE-345: Insufficient Verification of Data Authenticity",
          "level": "warning",
          "message": {
            "text": "DSR form: no email-ownership verification enables spoofed erasure requests  [CVSS 5.4: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N]"
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "src/pages/privacy/data-request.astro"
                },
                "region": {
                  "startLine": 47
                }
              }
            }
          ],
          "properties": {
            "severity": "medium",
            "security-severity": "5.4",
            "cvssRating": "Medium",
            "category": "CWE-345: Insufficient Verification of Data Authenticity",
            "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N",
            "cwe": "CWE-345 - Insufficient Verification of Data Authenticity",
            "cweId": "CWE-345",
            "cweName": "Insufficient Verification of Data Authenticity",
            "cvssScore": 5.4,
            "offensivePriority": "P3",
            "offensivePriorityLabel": "Internal Network / Privileged Position",
            "offensivePriorityReason": "Internal Network / Privileged Position | *exposure unverified \u2014 no CMDB context; AV:N (network-routable; internet exposure unconfirmed)*",
            "confidence": 0.82,
            "description": "#### Description\nThe 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 \u2014 and no server-side component exists to add one \u2014 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 \u2014 violating the procedural transparency GDPR Art.12(1) requires.\n\n#### Impact\nAny 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.\n\n#### Exploit scenario\nAttacker 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 \u2014 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.\n\n#### Preconditions\n- Attacker knows (or guesses) the email address of a registered data subject\n- Operator's manual verification procedure fails to require the data subject to reply from the claimed address or provide another proof of ownership\n\n```\n<form\n  class=\"dsr-form\"\n  action=\"mailto:privacy@aunova.ae\"\n  method=\"post\"\n  enctype=\"text/plain\"\n>\n  ...\n  <input id=\"email\" name=\"email\" type=\"email\" autocomplete=\"email\" required />\n  ...\n  <button type=\"submit\" class=\"dsr-submit\">Send request</button>\n</form>\n```\n\n#### How to fix\nAdd 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.\n\n**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 \u2014 static site\u2026"
          },
          "taxa": [
            {
              "toolComponent": {
                "name": "CWE",
                "guid": "b7c8d9e0-1f2a-3b4c-5d6e-7f8090a1b2c3"
              },
              "id": "CWE-345"
            }
          ],
          "rank": 54.0
        },
        {
          "ruleId": "CWE-20: Improper Input Validation",
          "level": "note",
          "message": {
            "text": "Empty-sentinel authEventId matched by relay-crafted OK message  [CVSS 3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L]"
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "src/lib/sendDM.ts"
                },
                "region": {
                  "startLine": 117
                }
              }
            }
          ],
          "properties": {
            "severity": "low",
            "security-severity": "3.1",
            "cvssRating": "Low",
            "category": "CWE-20: Improper Input Validation",
            "cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L",
            "cwe": "CWE-20 - Improper Input Validation",
            "cweId": "CWE-20",
            "cweName": "Improper Input Validation",
            "cvssScore": 3.1,
            "offensivePriority": "P3",
            "offensivePriorityLabel": "Internal Network / Privileged Position",
            "offensivePriorityReason": "Internal Network / Privileged Position | *exposure unverified \u2014 no CMDB context; AV:N (network-routable; internet exposure unconfirmed)*",
            "confidence": 0.78,
            "description": "#### Description\nauthEventId 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] \u2014 cast to string without format validation \u2014 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 \u2014 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.\n\n#### Impact\nA malicious or compromised relay can send ['OK','',false,'reason'] before any AUTH handshake has been initiated, causing the state machine to immediately call finish\u2192reject 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.\n\n#### Exploit scenario\nAn 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 \u2014 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.\n\n#### Preconditions\n- Attacker must operate or have compromised one of the five hardcoded relay WebSocket endpoints\n- 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\n\n```\nlet authEventId = \"\"; // line 117 \u2014 empty-string sentinel\n...\nif (id === authEventId) { // line 176 \u2014 relay-supplied data[1] matched with no guard\n  if (ok) {\n    authResent = true;\n    ws.send(JSON.stringify([\"EVENT\", event]));\n  } else {\n    finish(() => reject(new Error(`Auth rejected: ${reason}`))); // line 181\n  }\n  return;\n}\n```\n\n#### How to fix\nDo not use '' as the initial value of authEventId \u2014 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)`.\n\n**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 \u2014 relay cannot read pla\u2026"
          },
          "taxa": [
            {
              "toolComponent": {
                "name": "CWE",
                "guid": "b7c8d9e0-1f2a-3b4c-5d6e-7f8090a1b2c3"
              },
              "id": "CWE-20"
            }
          ],
          "rank": 31.0
        }
      ],
      "invocations": [
        {
          "executionSuccessful": true
        }
      ]
    }
  ]
}
