- Remove OCR noise, credits, and duplicates from rules-database.json (288→255 rules) - Add clean_rules.py script for rule cleanup - Add CLAUDE.md, docs/, and update README with documentation links Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
288 lines
5.6 KiB
Markdown
288 lines
5.6 KiB
Markdown
# Security Guide
|
|
|
|
This document outlines the security measures and best practices for Deathwatch Roller.
|
|
|
|
## Authentication
|
|
|
|
### Session Management
|
|
|
|
- Sessions use HTTP-only cookies
|
|
- Sessions expire after inactivity
|
|
- Passwords are hashed with bcrypt
|
|
- XSS protection headers enabled
|
|
|
|
### Session Configuration
|
|
|
|
```javascript
|
|
// Example session config
|
|
session: {
|
|
secret: process.env.SESSION_SECRET,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database Security
|
|
|
|
### Password Hashing
|
|
|
|
```typescript
|
|
// Password hashing
|
|
import bcrypt from 'bcrypt';
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 12);
|
|
const isMatch = await bcrypt.compare(inputPassword, hashedPassword);
|
|
```
|
|
|
|
### Prepared Statements
|
|
|
|
All database queries use parameterized statements to prevent SQL injection:
|
|
|
|
```typescript
|
|
// ✅ GOOD - Using parameterized query
|
|
const player = await Player.findOne({
|
|
where: { email: email },
|
|
raw: true
|
|
});
|
|
|
|
// ❌ BAD - String concatenation
|
|
const badQuery = `SELECT * FROM players WHERE email = '${email}'`;
|
|
```
|
|
|
|
## Input Validation
|
|
|
|
### Request Validation
|
|
|
|
All API requests are validated:
|
|
|
|
```typescript
|
|
import { body, validationResult } from 'express-validator';
|
|
|
|
router.post('/players', [
|
|
body('name').isLength({ min: 1, max: 50 }).trim(),
|
|
body('email').isEmail().normalizeEmail(),
|
|
body('xp').isInt({ min: 0 })
|
|
], async (req, res) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
// Process request
|
|
});
|
|
```
|
|
|
|
## CORS Configuration
|
|
|
|
```typescript
|
|
// CORS setup
|
|
import cors from 'cors';
|
|
|
|
app.use(cors({
|
|
origin: process.env.ALLOWED_ORIGINS?.split(','),
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
allowedHeaders: ['Content-Type', 'Authorization']
|
|
}));
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
```typescript
|
|
// Rate limiter
|
|
import rateLimit from 'express-rate-limit';
|
|
|
|
const limiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 100, // limit each IP to 100 requests per windowMs
|
|
message: 'Too many requests from this IP, please try again later.'
|
|
});
|
|
|
|
app.use('/api/', limiter);
|
|
```
|
|
|
|
## Security Headers
|
|
|
|
```typescript
|
|
import helmet from 'helmet';
|
|
|
|
app.use(helmet());
|
|
|
|
// Custom headers
|
|
app.use((req, res, next) => {
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
next();
|
|
});
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
Store sensitive data in environment variables:
|
|
|
|
```bash
|
|
# .env file
|
|
SESSION_SECRET=your-secure-random-string
|
|
X_GM_SECRET=your-gm-secret-key
|
|
DATABASE_PASSWORD=your-database-password
|
|
```
|
|
|
|
Never commit `.env` files:
|
|
|
|
```bash
|
|
# .gitignore
|
|
.env
|
|
.env.*
|
|
!/.env.example
|
|
```
|
|
|
|
## API Security
|
|
|
|
### GM Authentication
|
|
|
|
GM endpoints require special authentication:
|
|
|
|
```typescript
|
|
// GM secret header
|
|
app.use('/api/players/gm/*', (req, res, next) => {
|
|
const secret = req.headers['x-gm-secret'];
|
|
if (!secret || secret !== process.env.X_GM_SECRET) {
|
|
return res.status(403).json({ error: 'Forbidden' });
|
|
}
|
|
next();
|
|
});
|
|
```
|
|
|
|
### API Rate Limits
|
|
|
|
```typescript
|
|
const apiLimiter = rateLimit({
|
|
windowMs: 60 * 1000, // 1 minute
|
|
max: 100,
|
|
keyGenerator: (req) => req.ip
|
|
});
|
|
|
|
app.use('/api/', apiLimiter);
|
|
```
|
|
|
|
## File Upload Security
|
|
|
|
```typescript
|
|
import multer from 'multer';
|
|
|
|
const upload = multer({
|
|
storage: multer.diskStorage({
|
|
destination: (req, file, cb) => cb(null, 'uploads/'),
|
|
filename: (req, file, cb) => {
|
|
const uniqueName = `${Date.now()}-${Math.random()}-${file.originalname}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
}),
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type'));
|
|
}
|
|
},
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024 // 10MB
|
|
}
|
|
});
|
|
```
|
|
|
|
## Database Backups
|
|
|
|
```bash
|
|
# Backup script
|
|
#!/bin/bash
|
|
mysqldump -u user -p database > backup_$(date +%Y%m%d_%H%M%S).sql
|
|
# Rotate old backups
|
|
find . -name "backup_*.sql" -mtime +30 -delete
|
|
```
|
|
|
|
## Monitoring
|
|
|
|
Set up monitoring for security events:
|
|
|
|
```typescript
|
|
// Audit logging
|
|
import auditlog from 'express-auditlog';
|
|
|
|
app.use(auditlog());
|
|
```
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] Session cookies are HTTP-only
|
|
- [ ] Passwords are hashed with bcrypt
|
|
- [ ] SQL queries use parameterized statements
|
|
- [ ] CORS is properly configured
|
|
- [ ] Rate limiting is enabled
|
|
- [ ] Security headers are set
|
|
- [ ] Environment variables are not committed
|
|
- [ ] Input is validated on all endpoints
|
|
- [ ] File uploads are sanitized
|
|
- [ ] Dependencies are up to date
|
|
|
|
## Common Vulnerabilities
|
|
|
|
### SQL Injection Prevention
|
|
|
|
```typescript
|
|
// ✅ SAFE
|
|
User.findOne({ where: { email: email } });
|
|
|
|
// ❌ UNSAFE
|
|
User.findAll({ where: `email = '${email}'` });
|
|
```
|
|
|
|
### XSS Prevention
|
|
|
|
```typescript
|
|
// ✅ SAFE - Sanitize user input
|
|
const displayName = sanitize(userInput);
|
|
|
|
// ❌ UNSAFE - Direct output
|
|
res.send(userInput);
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
Keep dependencies updated:
|
|
|
|
```bash
|
|
# Check for updates
|
|
npm outdated
|
|
|
|
# Update all
|
|
npm update
|
|
|
|
# Update specific package
|
|
npm update express
|
|
```
|
|
|
|
## Security Audits
|
|
|
|
```bash
|
|
# Run security audit
|
|
npm audit
|
|
|
|
# Fix vulnerabilities
|
|
npm audit fix
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Principle of Least Privilege**: Use minimal permissions
|
|
2. **Defense in Depth**: Multiple security layers
|
|
3. **Fail Securely**: Default to safe responses
|
|
4. **Log Security Events**: Monitor for suspicious activity
|
|
5. **Regular Audits**: Periodic security reviews
|