diff --git a/SECURITY.md b/SECURITY.md index 628a7a6f..7046075e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,7 +21,11 @@ We enforce a strict CSP through both Nginx and React-level meta tags. - **Restrictions**: `'unsafe-inline'` is prohibited in production. - **Allowed Sources**: - Scripts/Styles: `'self'` - - API Connections: `https://*.stellar.org`, `https://api.coingecko.com` + - API Connections/Wallets: `https://*.stellar.org`, `wss://*.stellar.org`, `https://*.sorobanrpc.com`, `https://api.coingecko.com`, `wss://*.walletconnect.com`, `https://*.walletconnect.com`, `https://*.walletconnect.org`, `https://albedo.link`, `https://*.albedo.link` + - Inline Scripts: Allowed via strict SHA-256 hash validation for the theme initialization script. + +### Adding New Wallet or API Endpoints +To add a new endpoint or wallet integration, update the `Content-Security-Policy` header in `nginx.conf` and the corresponding meta tag in `index.html`. Add the domains to `connect-src` (for APIs/WebSocket) or `frame-src` (for iframes). ### 2. Automated Guardrails - **Dependabot**: Monitors `npm` and `github-actions` ecosystems daily for updates. diff --git a/index.html b/index.html index 0fe4149a..5bf921ec 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ + Stellar Developer Dashboard diff --git a/nginx.conf b/nginx.conf index b0094f36..beab4a61 100644 --- a/nginx.conf +++ b/nginx.conf @@ -9,7 +9,7 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://*.stellar.org https://api.coingecko.com; img-src 'self' data: https:; font-src 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests;" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-2cZtek/4Mta9v4mtLkqsK+tSR0rUJ9LWlEhKKvL5/Ts='; style-src 'self' https://fonts.googleapis.com; connect-src 'self' https://*.stellar.org wss://*.stellar.org https://*.sorobanrpc.com https://api.coingecko.com wss://*.walletconnect.com https://*.walletconnect.com https://*.walletconnect.org; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://albedo.link https://*.albedo.link; object-src 'none'; base-uri 'self'; upgrade-insecure-requests;" always; # Health check endpoint location /health { diff --git a/src/components/dashboard/Transactions.tsx b/src/components/dashboard/Transactions.tsx index fc481ec4..09879482 100644 --- a/src/components/dashboard/Transactions.tsx +++ b/src/components/dashboard/Transactions.tsx @@ -123,7 +123,7 @@ import { useAddressLabels } from '../../hooks/useAddressLabels' import { generateTransactionDescription } from '../../lib/aiTransactionDescription' const VIRTUAL_SCROLL_THRESHOLD = 200 -const PAGE_SIZE = 100 +const PAGE_SIZE = 50 function normalizeSearch(value) { return String(value || '').toLowerCase().trim() @@ -168,6 +168,38 @@ function flattenOperation(op) { } } +function InfiniteScrollSentinel({ onIntersect, hasMore, loading, label = 'items' }: { onIntersect: () => void, hasMore: boolean, loading: boolean, label?: string }) { + const sentinelRef = React.useRef(null) + + React.useEffect(() => { + if (!hasMore || loading) return + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + onIntersect() + } + }, { rootMargin: '200px' }) + + if (sentinelRef.current) { + observer.observe(sentinelRef.current) + } + + return () => observer.disconnect() + }, [onIntersect, hasMore, loading]) + + return ( +
+ {loading ? ( + Loading more... + ) : hasMore ? ( + Scroll for more + ) : ( + No more {label} + )} +
+ ) +} + function LoadingRows({ count, height }) { return (
@@ -596,29 +628,12 @@ export default function Transactions() {
))} -
- {txHasMore || txPagingLoading ? ( - - ) : ( - No more transactions - )} -
+ )} @@ -897,29 +912,12 @@ export default function Transactions() { ))} -
- {opsHasMore || opsPagingLoading ? ( - - ) : ( - No more operations - )} -
+ )} diff --git a/tests/csp.test.js b/tests/csp.test.js new file mode 100644 index 00000000..75899c20 --- /dev/null +++ b/tests/csp.test.js @@ -0,0 +1,55 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import assert from 'assert'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const nginxConfPath = path.resolve(__dirname, '../nginx.conf'); +const indexHtmlPath = path.resolve(__dirname, '../index.html'); + +console.log('Running CSP tests...'); + +try { + // Primary flow: nginx.conf should have a valid CSP header + const nginxContent = fs.readFileSync(nginxConfPath, 'utf8'); + const cspRegex = /add_header Content-Security-Policy "(.*)" always;/; + const match = nginxContent.match(cspRegex); + assert(match, 'CSP header missing in nginx.conf'); + const csp = match[1]; + + assert(csp.includes("default-src 'self'"), "CSP missing default-src 'self'"); + assert(csp.includes("script-src 'self'"), "CSP missing script-src 'self'"); + assert(csp.includes("connect-src"), "CSP missing connect-src"); + console.log('✅ Primary flow: nginx.conf valid'); + + // Primary flow: index.html should have a valid CSP meta tag + const htmlContent = fs.readFileSync(indexHtmlPath, 'utf8'); + const htmlCspRegex = //; + const htmlMatch = htmlContent.match(htmlCspRegex); + assert(htmlMatch, 'CSP meta tag missing in index.html'); + const htmlCsp = htmlMatch[1]; + + assert(htmlCsp.includes("default-src 'self'"), "HTML CSP missing default-src 'self'"); + assert(htmlCsp.includes("script-src 'self'"), "HTML CSP missing script-src 'self'"); + assert(htmlCsp.includes("connect-src"), "HTML CSP missing connect-src"); + console.log('✅ Primary flow: index.html valid'); + + // Boundary case: should allow required Stellar endpoints but not wildcard everything + assert(csp.includes("https://*.stellar.org"), "Missing stellar.org"); + assert(csp.includes("wss://*.walletconnect.com"), "Missing walletconnect.com"); + assert(!csp.match(/connect-src [^;]*\s\*(?:\s|;)/), "connect-src is too permissive with wildcard"); + console.log('✅ Boundary case: Required endpoints allowed securely'); + + // Failure case: should not allow arbitrary domains like http://evil.com + assert(!csp.includes("evil.com"), "CSP should not allow evil.com"); + assert(!csp.includes("http://"), "CSP should not allow http://"); + console.log('✅ Failure case: Malicious endpoints blocked'); + + console.log('All tests passed!'); + process.exit(0); +} catch (error) { + console.error('Test failed:', error.message); + process.exit(1); +}