SkateSpots/index.html
shakespeare.diy df6b6fdf92 Ship CSP in the bundle so it also protects the native (mobile) build
Follow-up to the map-XSS work. The Capacitor app ships the same dist/ bundle
(webDir: 'dist') and renders the same untrusted relay content, so it shared
the same injection surface — but the CSP only existed as HTTP headers
(public/_headers, netlify.toml), which never reach a WebView loading from a
local scheme. The prior "OS sandbox is enough" comment was wrong: the sandbox
isolates the app from OTHER apps; it does not stop a same-origin injected
script from reading this app's own localStorage (nsec, NWC string, Cashu seed)
or IndexedDB (Cashu bearer ecash proofs, native-only wallet).

Changes:
- index.html: add a <meta http-equiv="Content-Security-Policy"> so the policy
  ships inside the bundle and applies to BOTH web and native. script-src stays
  'self' 'wasm-unsafe-eval' (no unsafe-inline/eval). Capacitor local origins
  (https://localhost, capacitor://localhost, ionic://localhost) are allowlisted
  on script/style/img/connect so asset loading & live-reload still work.
  Replaces the old "CSP intentionally omitted" comment. Verified the app boots
  and the map renders with no CSP violations.
- SECURITY.md: correct the CSP section (now three synced locations incl. the
  meta tag; explain the local-scheme reason and the OS-sandbox misconception),
  add Cashu ecash/seed to the threat model and residual-risks, note the shared
  bundle, and extend the auditor file map (useCashu.ts, platform.ts,
  capacitor.config.ts).

Note: the actual injection fix (createElement + textContent in map popups)
already shipped on mobile since it lives in the shared bundle; this commit
adds the defense-in-depth layer that was previously web-only.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-16 22:30:19 +07:00

91 lines
4.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!--
Content-Security-Policy (defense-in-depth) — shipped in the HTML so it
applies to BOTH the web build AND the native Capacitor build.
Why a <meta> tag and not just HTTP headers: Capacitor loads the bundle
from a local scheme (https://localhost on Android, capacitor://localhost
on iOS), so the server headers in public/_headers / netlify.toml never
reach the mobile WebView. The OS sandbox isolates the app from OTHER apps;
it does NOT stop a same-origin injected script from reading this app's own
IndexedDB (Cashu ecash proofs) or localStorage (nsec, NWC string, Cashu
seed). A CSP is therefore just as important on mobile as on web.
Capacitor's local origins/schemes are allowlisted below so asset loading,
the local dev server, and live-reload continue to work. Notably there is
no 'unsafe-inline'/'unsafe-eval' on script-src (only 'wasm-unsafe-eval'
for the Bitcoin/Cashu WASM), which is the directive that actually blocks
injected <script>/onerror payloads. Keep this in sync with
public/_headers and netlify.toml.
-->
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
script-src 'self' 'wasm-unsafe-eval' https://localhost capacitor://localhost ionic://localhost;
style-src 'self' 'unsafe-inline' https://unpkg.com https://localhost capacitor://localhost ionic://localhost;
img-src 'self' data: blob: https: https://localhost capacitor://localhost ionic://localhost;
font-src 'self' data: https://fonts.gstatic.com;
connect-src 'self' https: wss: https://localhost capacitor://localhost ionic://localhost;
media-src 'self' blob: https:;
frame-src 'self' https:;
worker-src 'self' blob:;
manifest-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self'
">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.webmanifest">
<!-- App Icons -->
<link rel="icon" type="image/png" href="/images/icon-1024.png">
<link rel="apple-touch-icon" href="/images/icon-1024.png">
<link rel="apple-touch-icon" sizes="152x152" href="/images/icon-1024.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icon-1024.png">
<link rel="apple-touch-startup-image" href="/images/icon-1024.png">
<!-- Theme Color -->
<meta name="theme-color" content="#ff0040">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="SkateSpots">
<!-- SEO Meta Tags -->
<meta name="description" content="Discover skate spots, find coaches, join events, and connect with the skateboarding community on Nostr. Free, decentralized, censorship-resistant.">
<meta name="keywords" content="skateboarding, skate spots, skate parks, nostr, decentralized, bitcoin, lightning, zaps">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:title" content="SkateSpots - Decentralized Skate Map & Community">
<meta property="og:description" content="Discover skate spots, find coaches, join events, and connect with the skateboarding community on Nostr.">
<meta property="og:image" content="https://skate-spots.org/images/icon-1024.png">
<meta property="og:url" content="https://skate-spots.org">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="SkateSpots - Decentralized Skate Map">
<meta name="twitter:description" content="Discover skate spots, find coaches, join events on Nostr.">
<meta name="twitter:image" content="https://skate-spots.org/images/icon-1024.png">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
</head>
<body>
<div id="root"></div>
<!--
BOOT CRASH GUARD (native-safe) — external so the web build can ship a
strict CSP (script-src 'self') with no 'unsafe-inline'. See boot-guard.js.
-->
<script src="/boot-guard.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>