Security considerations and best practices for Crypto Data Aggregator.
- Overview
- API Security
- Rate Limiting
- Data Handling
- Client-Side Security
- Deployment Security
- Reporting Vulnerabilities
Crypto Data Aggregator follows security best practices:
- No API keys required - Uses public APIs
- No user accounts - All data stored client-side
- No sensitive data - No PII collected
- Edge Runtime - Reduced attack surface
All external API calls are proxied through Next.js API routes:
Client → /api/market/coins → CoinGecko API
Benefits:
- Hides external API endpoints from clients
- Enables rate limiting at proxy level
- Allows response transformation
- Enables caching
All API responses include security headers:
// src/lib/api-utils.ts
export function jsonResponse(data: unknown, options?: ResponseOptions) {
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=60, stale-while-revalidate=300',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
},
});
}// next.config.js
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: process.env.ALLOWED_ORIGIN || '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' },
],
},
];
}All /api/v2/* endpoints now include built-in rate limiting with response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200Default Limits:
- News/Articles: 100 requests/minute
- Market Data: 60 requests/minute
- AI Digest: 20 requests/minute
When rate limited, API returns 429 Too Many Requests with Retry-After header.
| API | Rate Limit | Implementation |
|---|---|---|
| CoinGecko | 10-30 req/min | Server-side caching |
| DeFiLlama | Unlimited | Cache for efficiency |
| Alternative.me | ~10 req/min | 5-minute cache |
// SWR configuration
const { data } = useSWR('/api/market/coins', fetcher, {
refreshInterval: 60000, // 1 minute
dedupingInterval: 30000, // 30 seconds
revalidateOnFocus: false,
});// Cache responses to reduce API calls
import { newsCache } from '@/lib/cache';
export async function getNews() {
const cached = newsCache.get('latest');
if (cached) return cached;
const data = await fetchFromAPI();
newsCache.set('latest', data, 300); // 5 min TTL
return data;
}For self-hosted deployments:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const rateLimit = new Map<string, { count: number; resetTime: number }>();
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const ip = request.ip ?? 'anonymous';
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 100;
const current = rateLimit.get(ip);
if (!current || now > current.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
return NextResponse.next();
}
if (current.count >= maxRequests) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(Math.ceil((current.resetTime - now) / 1000)),
},
});
}
current.count++;
return NextResponse.next();
}All user data is stored in browser localStorage:
| Data Type | Storage Key | Sensitive |
|---|---|---|
| Watchlist | watchlist |
No |
| Portfolio | portfolios |
Low (no amounts) |
| Alerts | price_alerts |
No |
| Bookmarks | bookmarks |
No |
| Preferences | theme |
No |
All user input is validated:
// Portfolio validation example
export function addHolding(portfolioId: string, holding: { coinId: string; amount: number }) {
// Validate inputs
if (!portfolioId || typeof portfolioId !== 'string') {
throw new Error('Invalid portfolio ID');
}
if (!holding.coinId || typeof holding.coinId !== 'string') {
throw new Error('Invalid coin ID');
}
if (typeof holding.amount !== 'number' || holding.amount < 0) {
throw new Error('Invalid amount');
}
// Sanitize coinId (alphanumeric + hyphens only)
const sanitizedCoinId = holding.coinId.replace(/[^a-z0-9-]/gi, '');
// ... rest of implementation
}React automatically escapes content. For raw HTML:
// ❌ Dangerous
<div dangerouslySetInnerHTML={{ __html: userContent }} />;
// ✅ Safe - sanitize first
import DOMPurify from 'dompurify';
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(userContent),
}}
/>;// Strip potentially dangerous fields from API responses
function sanitizeApiResponse(data: unknown): unknown {
if (typeof data !== 'object' || data === null) {
return data;
}
if (Array.isArray(data)) {
return data.map(sanitizeApiResponse);
}
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
// Skip potentially dangerous fields
if (['__proto__', 'constructor', 'prototype'].includes(key)) {
continue;
}
sanitized[key] = sanitizeApiResponse(value);
}
return sanitized;
}Configure CSP in next.config.js:
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // Required for Next.js
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.coingecko.com https://api.llama.fi",
"frame-ancestors 'none'",
].join('; '),
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
];For external scripts (if any):
<script src="https://example.com/script.js" integrity="sha384-..." crossorigin="anonymous"></script>// Always use rel="noopener noreferrer" for external links
<a href={article.url} target="_blank" rel="noopener noreferrer">
Read More
</a># .env.local (never commit!)
COINGECKO_API_KEY=cg-xxxxx # Optional, for higher rate limits
WEBHOOK_SECRET=your-secret # For webhook verification
ALLOWED_ORIGINS=https://yourdomain.com// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
}
]
}# Use non-root user
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
# ... build steps ...
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Always deploy with HTTPS. For self-hosted:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}# Check for vulnerabilities
npm audit
# Fix automatically where possible
npm audit fix
# Check for outdated packages
npm outdatedUse Dependabot or Renovate:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
groups:
production-dependencies:
dependency-type: 'production'
development-dependencies:
dependency-type: 'development'If you discover a security vulnerability:
- DO NOT open a public GitHub issue
- Email security concerns to the repository owner
- Include detailed reproduction steps
- Allow reasonable time for a fix before disclosure
Report security issues via GitHub Security Advisories:
- Go to repository → Security → Advisories
- Click "New draft security advisory"
- Provide detailed information
- Validate all user inputs
- Sanitize data before rendering
- Use parameterized queries (if using DB)
- Keep dependencies updated
- Review npm audit regularly
- HTTPS enabled
- Security headers configured
- Environment variables secured
- Rate limiting enabled
- Logging configured
- Error messages don't leak info
- Monitor for unusual traffic patterns
- Set up alerts for error spikes
- Review access logs periodically
- Check for dependency vulnerabilities