This document outlines the security assumptions, potential attack vectors, and mitigation strategies for the PromptHash Stellar ecosystem.
The system relies on a hybrid architecture combining on-chain state (Soroban) with off-chain gated delivery (Unlock Service).
- Client (Browser): Responsible for initial encryption and wallet interaction. Trusted to not leak the plaintext before it's encrypted.
- Soroban Contract: Trusted source of truth for "who owns what". Enforces XLM payments and immutable entitlement records.
- Unlock Service: Responsible for key unwrapping and decryption. Trusted to verify on-chain state before releasing content.
Scenario: An attacker intercepts a signed challenge and attempts to use it later to unlock content. Mitigation:
- Nonces: Every challenge includes a unique
nonce(UUID) that the server tracks (or signs into the token). - TTL (Time-to-Live): Challenge tokens are short-lived (e.g., 5 minutes). Even if intercepted, the window of opportunity is small.
- Server Signature: The challenge token is signed by the server's secret, preventing attackers from forging their own valid challenges.
Scenario: A user attempts to unlock content without paying, or after a transaction was reverted. Mitigation:
- On-Chain Verification: The Unlock Service MUST query the Soroban contract's
has_accessmethod before performing any decryption. This ensures that the buyer's address is permanently recorded as having purchase rights. - Finality: The service should wait for transaction finality (successful ledger inclusion) before acknowledging a purchase.
Scenario: An attacker gains access to the Unlock Service's private key. Mitigation:
- Encrypted-at-Rest: Content stored on-chain is encrypted with AES keys that are wrapped. Even with the service private key, the attacker still needs to fetch the encrypted payload from the blockchain.
- Separation of Concerns: The service does not store a master key for all prompts; it only holds the key used for wrapping.
Scenario: A creator sells a "Gold Prompt" but puts garbage in the encrypted payload. Mitigation:
- Content Hash: The contract stores a SHA-256 hash of the intended plaintext. When the buyer unlocks, the service re-hashes the result. If it doesn't match, the buyer has proof of fraud.
- Reputation: (Future) Community ratings and escrow systems can mitigate this further.
The has_access logic in the contract is the primary gatekeeper:
fn has_access(env: Env, user: Address, prompt_id: u128) -> Result<bool, Error> {
let prompt = Storage::require_prompt(&env, prompt_id)?;
Ok(prompt.creator == user || Storage::has_purchase(&env, prompt_id, &user))
}This ensures that ONLY the original creator or a verified buyer can ever trigger the unlock flow successfully.
The frontend ships with baseline browser security headers to reduce XSS, clickjacking, and data leakage risks.
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
See policy below | Restricts resource loading to trusted origins |
X-Frame-Options |
DENY |
Prevents clickjacking via iframe embedding |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), interest-cohort=() |
Disables unnecessary browser features |
X-XSS-Protection |
0 |
Disables legacy XSS filter (CSP is the modern mitigation) |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Forces HTTPS for 2 years |
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: blob: https://gateway.pinata.cloud https://*.sentry.io;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://soroban-*.stellar.org https://soroban-rpc.mainnet.stellar.org
https://horizon-*.stellar.org https://horizon.stellar.org
https://rpc-futurenet.stellar.org
https://friendbot*.stellar.org https://friendbot.stellar.org
https://gateway.pinata.cloud https://api.pinata.cloud
https://*.sentry.io https://secret-ai-gateway.onrender.com
wss://*.sentry.io;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
worker-src 'none';
upgrade-insecure-requests
| Directive | Origins | Why |
|---|---|---|
connect-src |
soroban-*.stellar.org, soroban-rpc.mainnet.stellar.org |
Stellar RPC endpoints (testnet/futurenet via wildcard, mainnet explicit) |
connect-src |
horizon-*.stellar.org, horizon.stellar.org |
Horizon API endpoints (testnet/futurenet via wildcard, mainnet explicit) |
connect-src |
rpc-futurenet.stellar.org |
Futurenet Soroban RPC (different subdomain pattern than testnet) |
connect-src |
friendbot*.stellar.org, friendbot.stellar.org |
Stellar testnet/friendbot faucet |
connect-src |
gateway.pinata.cloud, api.pinata.cloud |
IPFS upload (Pinata) and ciphertext retrieval |
connect-src |
*.sentry.io, wss://*.sentry.io |
Error monitoring and session replay |
connect-src |
secret-ai-gateway.onrender.com |
Chat/AI API backend |
img-src |
gateway.pinata.cloud |
IPFS-hosted prompt images and avatars |
style-src |
fonts.googleapis.com |
Google Fonts CSS (Inter, Inconsolata) |
font-src |
fonts.gstatic.com |
Google Fonts font files |
style-src 'unsafe-inline': Required because React and Radix UI inject styles at runtime viaelement.styleandCSSStyleSheet.insertRule(). CSP nonces do not cover these patterns. This is standard for React SPAs.script-src: No'unsafe-inline'is used in production. All scripts are external module files served with hashes by Vite. The dev CSP adds'unsafe-eval'for Vite HMR and'unsafe-inline'for dev tooling.
The Vite dev server injects a more permissive CSP via scripts/vite-security-headers.mjs:
- Adds
'unsafe-eval'and'unsafe-inline'toscript-src(required by Vite HMR) - Adds
ws:andwss:toconnect-src(Vite WebSocket for HMR) - Adds
http://localhost:5173andhttp://localhost:5000(dev server and API proxy) - Adds
blob:toworker-src(service worker support in dev)
Production headers are served exclusively through vercel.json and additionally include:
upgrade-insecure-requests(auto-upgrades HTTP to HTTPS)- No
'unsafe-eval'or'unsafe-inline'inscript-src
When adding a new external service:
- Identify which CSP directive the service falls under (connect-src for API calls, img-src for images, etc.)
- Add the service's origin to
vercel.jsonheaders andscripts/vite-security-headers.mjs - Run
vitest run src/test/security-headers.test.tsto verify the test still passes - Update this document's policy table
- Test the affected flow in browser devtools (Network and Console tabs)