This document outlines the security measures implemented in the contract event indexer and recommendations for production deployment.
Status: ✅ Implemented
All database queries use parameterized statements to prevent SQL injection attacks:
// ✅ SAFE - Parameterized query
await client.query(
'SELECT * FROM contract_events WHERE contract_id = $1 AND ledger = $2',
[contractId, ledger]
);
// ❌ NEVER DO THIS - String concatenation
await client.query(
`SELECT * FROM contract_events WHERE contract_id = '${contractId}'`
);Test Coverage: SQL injection attempts are tested in tests/indexer/service.replay.test.ts
Status: ✅ Implemented
All replay request parameters are validated before processing:
contract_id: Must be non-empty stringledger: Must be non-negative integerfrom_block: Must be non-negative integer (if provided)to_block: Must be non-negative integer (if provided)from_blockmust be ≤to_block
Invalid inputs are rejected with descriptive error messages.
Status: ✅ Implemented
All replay operations use database transactions:
- Success: Changes committed atomically
- Failure: Automatic rollback prevents partial updates
- Connection errors: Proper cleanup and resource release
Status: ✅ Implemented
Only one replay operation can run at a time to prevent:
- Database connection pool exhaustion
- Memory pressure from multiple large operations
- Conflicting progress state
Limitation: Current implementation uses in-memory state. For multi-instance deployments, use Redis or database-backed locking.
Status: ✅ Implemented
- Connection pooling with configurable limits
- Automatic connection release in finally blocks
- Graceful shutdown handling
- Batch size limits to prevent memory exhaustion
Status:
The /internal/indexer/* endpoints are not authenticated by default.
Required Actions:
// Example: Add authentication middleware
import { authenticate, authorize } from './middleware/auth';
// Require authentication for all internal endpoints
app.use('/internal', authenticate);
// Require specific role for replay operations
app.post('/internal/indexer/events/replay',
authorize(['admin', 'indexer-operator']),
replayHandler
);Recommended Solutions:
- JWT tokens with role-based access control
- API keys with rate limiting
- OAuth 2.0 for service-to-service authentication
- mTLS for internal service communication
Status:
Required Actions:
import rateLimit from 'express-rate-limit';
const replayLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 replay requests per window
message: 'Too many replay requests, please try again later'
});
app.post('/internal/indexer/events/replay', replayLimiter, replayHandler);Status:
Required Actions:
const allowedIPs = process.env.ALLOWED_IPS?.split(',') || [];
function ipWhitelist(req, res, next) {
const clientIP = req.ip || req.connection.remoteAddress;
if (!allowedIPs.includes(clientIP)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
}
app.use('/internal', ipWhitelist);Status:
Required Actions:
- Deploy behind a reverse proxy (nginx, HAProxy)
- Use Let's Encrypt for TLS certificates
- Enforce HTTPS redirects
- Enable HSTS headers
Status:
Current: Environment variables in .env file
Production Recommendations:
- Use AWS Secrets Manager, HashiCorp Vault, or similar
- Rotate database credentials regularly
- Never commit
.envfiles to version control - Use different credentials per environment
Status:
Required Actions:
function auditLog(action: string, userId: string, details: any) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
action,
userId,
details,
ip: req.ip,
}));
}
// Log all replay operations
app.post('/internal/indexer/events/replay', async (req, res) => {
auditLog('replay_started', req.user.id, {
contract_id: req.body.contract_id,
ledger: req.body.ledger,
});
// ... rest of handler
});Run security-focused tests:
# SQL injection prevention tests
pnpm test -- --testNamePattern="SQL Injection"
# Input validation tests
pnpm test -- --testNamePattern="Input Validation"
# Concurrent operation tests
pnpm test -- --testNamePattern="Concurrent"- All database queries use parameterized statements
- Input validation on all endpoints
- Authentication enabled on
/internal/*endpoints - Rate limiting configured
- IP whitelisting enabled (if applicable)
- HTTPS/TLS enabled
- Secrets stored securely (not in code)
- Audit logging enabled
- Error messages don't leak sensitive information
- Database credentials have minimal required permissions
If you discover a security vulnerability, please:
- DO NOT open a public GitHub issue
- Email security@yourcompany.com with details
- Include steps to reproduce
- Allow 90 days for patching before public disclosure
The database user should have minimal permissions:
-- Create dedicated user for indexer
CREATE USER indexer_app WITH PASSWORD 'secure_password';
-- Grant only required permissions
GRANT CONNECT ON DATABASE indexer_db TO indexer_app;
GRANT SELECT, INSERT ON historical_events TO indexer_app;
GRANT SELECT, INSERT ON contract_events TO indexer_app;
-- DO NOT grant DELETE, DROP, or superuser privileges- Use SSL/TLS for database connections
- Rotate credentials regularly
- Use connection pooling with limits
- Monitor for unusual query patterns
- Encrypt sensitive data at rest
- Use PostgreSQL row-level security if needed
- Regular backups with encryption
- Test backup restoration procedures
Set up alerts for:
- Failed authentication attempts
- Unusual replay patterns (frequency, size)
- Database connection errors
- High memory/CPU usage
- Slow query performance
Depending on your use case, consider:
- GDPR: If processing EU user data
- SOC 2: For service providers
- PCI DSS: If handling payment data
- HIPAA: If handling health data
- Keep dependencies updated:
pnpm audit - Monitor security advisories for PostgreSQL
- Subscribe to Node.js security releases
- Review and update this policy quarterly
For security questions or concerns:
- Email: security@yourcompany.com
- Security team: [Your security team contact]
Last Updated: 2026-05-28 Next Review: 2026-08-28