|
| 1 | +# Security Patterns for Safe Rendering |
| 2 | + |
| 3 | +This document outlines security best practices for the frontend application to prevent XSS and other injection attacks. |
| 4 | + |
| 5 | +## Core Principles |
| 6 | + |
| 7 | +### 1. Trust React's Automatic Escaping |
| 8 | + |
| 9 | +React automatically escapes all values rendered in JSX: |
| 10 | + |
| 11 | +```tsx |
| 12 | +// ✅ SAFE - React escapes the value |
| 13 | +const userInput = '<script>alert(1)</script>'; |
| 14 | +<div>{userInput}</div> |
| 15 | +// Renders: <script>alert(1)</script> |
| 16 | + |
| 17 | +// ✅ SAFE - Attributes are also escaped |
| 18 | +<div title={userInput}>Content</div> |
| 19 | +``` |
| 20 | + |
| 21 | +### 2. Never Use Dangerous APIs |
| 22 | + |
| 23 | +```tsx |
| 24 | +// ❌ NEVER DO THIS |
| 25 | +<div dangerouslySetInnerHTML={{ __html: userInput }} /> |
| 26 | + |
| 27 | +// ❌ NEVER DO THIS |
| 28 | +element.innerHTML = userInput; |
| 29 | + |
| 30 | +// ❌ NEVER DO THIS |
| 31 | +eval(userInput); |
| 32 | +new Function(userInput)(); |
| 33 | +``` |
| 34 | + |
| 35 | +### 3. Validate External Data |
| 36 | + |
| 37 | +Always validate data from external sources: |
| 38 | + |
| 39 | +```typescript |
| 40 | +import { isValidTransactionHash, isValidStellarAddress } from './lib/security'; |
| 41 | + |
| 42 | +// ✅ SAFE - Validate before using |
| 43 | +if (isValidTransactionHash(hash)) { |
| 44 | + const url = getStellarExplorerUrl(hash); |
| 45 | +} |
| 46 | + |
| 47 | +// ✅ SAFE - Validate addresses |
| 48 | +if (isValidStellarAddress(address)) { |
| 49 | + // Use address |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +### 4. Sanitize External URLs |
| 54 | + |
| 55 | +```typescript |
| 56 | +import { sanitizeExternalLink } from './lib/security'; |
| 57 | + |
| 58 | +// ✅ SAFE - Sanitize before using |
| 59 | +const safeUrl = sanitizeExternalLink(userProvidedUrl); |
| 60 | +<a href={safeUrl} target="_blank" rel="noopener noreferrer">Link</a> |
| 61 | +``` |
| 62 | + |
| 63 | +## Common Patterns |
| 64 | + |
| 65 | +### Rendering API Data |
| 66 | + |
| 67 | +```tsx |
| 68 | +// ✅ SAFE - TypeScript interfaces + React escaping |
| 69 | +interface Transaction { |
| 70 | + id: string; |
| 71 | + amount: string; |
| 72 | + asset: string; |
| 73 | +} |
| 74 | + |
| 75 | +function TransactionRow({ tx }: { tx: Transaction }) { |
| 76 | + return ( |
| 77 | + <tr> |
| 78 | + <td>{tx.id}</td> |
| 79 | + <td>{tx.amount}</td> |
| 80 | + <td>{tx.asset}</td> |
| 81 | + </tr> |
| 82 | + ); |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +### Rendering User Input |
| 87 | + |
| 88 | +```tsx |
| 89 | +// ✅ SAFE - Controlled inputs |
| 90 | +function AmountInput() { |
| 91 | + const [amount, setAmount] = useState(''); |
| 92 | + |
| 93 | + return ( |
| 94 | + <input |
| 95 | + value={amount} |
| 96 | + onChange={(e) => setAmount(e.target.value)} |
| 97 | + /> |
| 98 | + ); |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +### Building URLs |
| 103 | + |
| 104 | +```tsx |
| 105 | +import { getStellarExplorerUrl } from './lib/security'; |
| 106 | + |
| 107 | +// ✅ SAFE - Use security utilities |
| 108 | +function TransactionLink({ hash }: { hash: string }) { |
| 109 | + const url = getStellarExplorerUrl(hash, 'testnet'); |
| 110 | + |
| 111 | + return ( |
| 112 | + <a href={url} target="_blank" rel="noopener noreferrer"> |
| 113 | + View Transaction |
| 114 | + </a> |
| 115 | + ); |
| 116 | +} |
| 117 | + |
| 118 | +// ❌ UNSAFE - Direct concatenation without validation |
| 119 | +function UnsafeLink({ hash }: { hash: string }) { |
| 120 | + return <a href={`https://example.com/tx/${hash}`}>Link</a>; |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### Displaying Error Messages |
| 125 | + |
| 126 | +```tsx |
| 127 | +// ✅ SAFE - Error objects are typed |
| 128 | +function ErrorDisplay({ error }: { error: Error }) { |
| 129 | + return ( |
| 130 | + <div className="error"> |
| 131 | + <p>{error.message}</p> |
| 132 | + </div> |
| 133 | + ); |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### Rendering Lists |
| 138 | + |
| 139 | +```tsx |
| 140 | +// ✅ SAFE - Array.map with React escaping |
| 141 | +function AssetList({ assets }: { assets: string[] }) { |
| 142 | + return ( |
| 143 | + <ul> |
| 144 | + {assets.map((asset, i) => ( |
| 145 | + <li key={i}>{asset}</li> |
| 146 | + ))} |
| 147 | + </ul> |
| 148 | + ); |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +## Anti-Patterns to Avoid |
| 153 | + |
| 154 | +### ❌ String Concatenation for HTML |
| 155 | + |
| 156 | +```tsx |
| 157 | +// ❌ NEVER DO THIS |
| 158 | +const html = '<div>' + userInput + '</div>'; |
| 159 | +element.innerHTML = html; |
| 160 | + |
| 161 | +// ✅ DO THIS INSTEAD |
| 162 | +<div>{userInput}</div> |
| 163 | +``` |
| 164 | + |
| 165 | +### ❌ Unvalidated External URLs |
| 166 | + |
| 167 | +```tsx |
| 168 | +// ❌ UNSAFE |
| 169 | +<a href={userProvidedUrl}>Link</a> |
| 170 | + |
| 171 | +// ✅ SAFE |
| 172 | +import { sanitizeExternalLink } from './lib/security'; |
| 173 | +<a href={sanitizeExternalLink(userProvidedUrl)}>Link</a> |
| 174 | +``` |
| 175 | + |
| 176 | +### ❌ Dynamic Event Handlers from Strings |
| 177 | + |
| 178 | +```tsx |
| 179 | +// ❌ NEVER DO THIS |
| 180 | +<button onClick={eval(userInput)}>Click</button> |
| 181 | + |
| 182 | +// ✅ DO THIS INSTEAD |
| 183 | +<button onClick={() => handleClick()}>Click</button> |
| 184 | +``` |
| 185 | + |
| 186 | +### ❌ Unvalidated Data in Template Literals |
| 187 | + |
| 188 | +```tsx |
| 189 | +// ❌ POTENTIALLY UNSAFE |
| 190 | +const url = `https://api.example.com/${userInput}`; |
| 191 | + |
| 192 | +// ✅ SAFE - Validate first |
| 193 | +import { isAlphanumericSafe } from './lib/security'; |
| 194 | +if (isAlphanumericSafe(userInput)) { |
| 195 | + const url = `https://api.example.com/${userInput}`; |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +## Security Utilities |
| 200 | + |
| 201 | +### Available Functions |
| 202 | + |
| 203 | +```typescript |
| 204 | +// Transaction hash validation |
| 205 | +isValidTransactionHash(hash: string): boolean |
| 206 | + |
| 207 | +// Stellar address validation |
| 208 | +isValidStellarAddress(address: string): boolean |
| 209 | + |
| 210 | +// URL sanitization |
| 211 | +sanitizeExternalUrl(url: string, allowedDomains: string[]): string |
| 212 | +sanitizeExternalLink(url: string): string |
| 213 | + |
| 214 | +// Safe URL construction |
| 215 | +getStellarExplorerUrl(hash: string, network: 'testnet' | 'mainnet'): string |
| 216 | + |
| 217 | +// HTML escaping (rarely needed in React) |
| 218 | +escapeHtml(text: string): string |
| 219 | + |
| 220 | +// Alphanumeric validation |
| 221 | +isAlphanumericSafe(text: string): boolean |
| 222 | + |
| 223 | +// Safe truncation |
| 224 | +truncateSafe(text: string, maxLength: number): string |
| 225 | +``` |
| 226 | + |
| 227 | +### Usage Examples |
| 228 | + |
| 229 | +```typescript |
| 230 | +import { |
| 231 | + isValidTransactionHash, |
| 232 | + getStellarExplorerUrl, |
| 233 | + sanitizeExternalLink, |
| 234 | +} from './lib/security'; |
| 235 | + |
| 236 | +// Validate transaction hash |
| 237 | +if (isValidTransactionHash(hash)) { |
| 238 | + const url = getStellarExplorerUrl(hash, 'testnet'); |
| 239 | + window.open(url, '_blank'); |
| 240 | +} |
| 241 | + |
| 242 | +// Sanitize external link |
| 243 | +const safeUrl = sanitizeExternalLink(userUrl); |
| 244 | +if (safeUrl !== '#') { |
| 245 | + // URL is safe to use |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +## Testing |
| 250 | + |
| 251 | +### Run XSS Prevention Tests |
| 252 | + |
| 253 | +```bash |
| 254 | +npm test -- xss-prevention |
| 255 | +``` |
| 256 | + |
| 257 | +### Run Security Utility Tests |
| 258 | + |
| 259 | +```bash |
| 260 | +npm test -- security.test |
| 261 | +``` |
| 262 | + |
| 263 | +### Test Coverage |
| 264 | + |
| 265 | +The test suite includes: |
| 266 | +- 17+ common XSS attack vectors |
| 267 | +- React component rendering tests |
| 268 | +- Form input validation tests |
| 269 | +- URL sanitization tests |
| 270 | +- Integration tests |
| 271 | + |
| 272 | +## ESLint Security Rules |
| 273 | + |
| 274 | +The following ESLint rules are enforced: |
| 275 | + |
| 276 | +```javascript |
| 277 | +{ |
| 278 | + 'no-eval': 'error', |
| 279 | + 'no-implied-eval': 'error', |
| 280 | + 'no-new-func': 'error', |
| 281 | + 'no-script-url': 'error', |
| 282 | +} |
| 283 | +``` |
| 284 | + |
| 285 | +## Content Security Policy |
| 286 | + |
| 287 | +When deploying, add these CSP headers: |
| 288 | + |
| 289 | +``` |
| 290 | +Content-Security-Policy: |
| 291 | + default-src 'self'; |
| 292 | + script-src 'self'; |
| 293 | + style-src 'self' 'unsafe-inline'; |
| 294 | + img-src 'self' data: https:; |
| 295 | + connect-src 'self' https://horizon-testnet.stellar.org https://horizon.stellar.org; |
| 296 | + frame-ancestors 'none'; |
| 297 | + base-uri 'self'; |
| 298 | + form-action 'self'; |
| 299 | +``` |
| 300 | + |
| 301 | +## Checklist for New Features |
| 302 | + |
| 303 | +When adding new features, verify: |
| 304 | + |
| 305 | +- [ ] No use of `dangerouslySetInnerHTML` |
| 306 | +- [ ] No use of `innerHTML`, `outerHTML`, or `insertAdjacentHTML` |
| 307 | +- [ ] No use of `eval()`, `Function()`, or `setTimeout(string)` |
| 308 | +- [ ] External URLs are validated with `sanitizeExternalLink()` |
| 309 | +- [ ] Transaction hashes are validated with `isValidTransactionHash()` |
| 310 | +- [ ] User input is rendered through React JSX (automatic escaping) |
| 311 | +- [ ] External links have `rel="noopener noreferrer"` |
| 312 | +- [ ] Tests cover malicious input scenarios |
| 313 | + |
| 314 | +## Resources |
| 315 | + |
| 316 | +- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) |
| 317 | +- [React Security Best Practices](https://react.dev/learn/writing-markup-with-jsx) |
| 318 | +- [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) |
| 319 | +- [Stellar Security Guidelines](https://developers.stellar.org/docs/building-apps/security) |
| 320 | + |
| 321 | +## Questions? |
| 322 | + |
| 323 | +If you're unsure whether a pattern is safe, ask yourself: |
| 324 | + |
| 325 | +1. Does this use any dangerous APIs? (`dangerouslySetInnerHTML`, `eval`, `innerHTML`) |
| 326 | +2. Is external data validated before use? |
| 327 | +3. Are URLs sanitized before rendering? |
| 328 | +4. Is React's automatic escaping being bypassed? |
| 329 | + |
| 330 | +If the answer to any of these is "yes" or "maybe", review this document or consult the security utilities. |
0 commit comments