// ❌ BEFORE: raw HTML injection
<h2>{image.title}</h2>
// If title is: <script>steal()</script>
// → XSS! Script runs in every user's browser
// ✅ React auto-escapes by default
// But watch for dangerouslySetInnerHTML:
// NEVER use it with user input
// Server-side: sanitize on input
const sanitizeHtml =
require('sanitize-html');
const cleanTitle = sanitizeHtml(
req.body.title, {
allowedTags: [], // No HTML
allowedAttributes: {} // Nothing
});
const fileType =
require('file-type-cjs');
async function validateImageFile(
filePath
) {
const type =
await fileType.fromFile(filePath);
const ALLOWED_TYPES = [
'image/jpeg', 'image/png',
'image/webp', 'image/gif',
];
if (!type ||
!ALLOWED_TYPES.includes(type.mime)) {
// Delete the uploaded file
fs.unlinkSync(filePath);
throw new Error(
'Invalid file type. ' +
'Only images allowed.');
}
return type;
}
// Use in upload route
app.post('/api/images/upload',
authenticate,
upload.single('image'),
async (req, res) => {
await validateImageFile(
req.file.path);
// ... proceed with valid image
});
npm install helmet
const helmet = require('helmet');
app.use(helmet());
// Custom Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: [
"'self'",
"https://fonts.googleapis.com"
],
imgSrc: [
"'self'", "data:", "blob:"
],
connectSrc: [
"'self'",
"wss://pixelcraft.app" // WebSocket
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com"
],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
}
}));
// ❌ BEFORE: wide open
app.use(cors({ origin: '*' }));
// ✅ AFTER: whitelist specific origins
app.use(cors({
origin: [
process.env.FRONTEND_URL,
'https://pixelcraft.app',
],
credentials: true,
methods: ['GET', 'POST', 'PUT',
'DELETE', 'PATCH'],
}));
// Move ALL secrets to .env
// .env (NEVER commit this file)
DATABASE_URL=mongodb://...
JWT_SECRET=generated-random-64-chars
RESEND_API_KEY=re_...
REDIS_URL=redis://...
// .gitignore
.env
.env.local
.env.production
// Stricter rate limiting for auth
async function authRateLimit(
req, res, next
) {
const key =
`auth-limit:${req.ip}`;
const current =
await redis.incr(key);
if (current === 1) {
await redis.expire(key, 900);
// 15-minute window
}
if (current > 10) {
// 10 attempts per 15 minutes
return res.status(429).json({
error: 'Too many login attempts. '
+ 'Try again in 15 minutes.',
});
}
next();
}
app.use('/api/auth/login',
authRateLimit);
app.use('/api/auth/forgot-password',
authRateLimit);
git switch -c fix/PIXELCRAFT-072-security-audit
git add server/ .gitignore
git commit -m "Fix 5 security vulnerabilities (PIXELCRAFT-072)"
git push origin fix/PIXELCRAFT-072-security-audit
# PR → Security review → Merge → Close ticket ✅
Security is not a feature — it's a constraint on every feature.
The OWASP Top 10 has barely changed in 20 years. The same classes of vulnerabilities keep appearing because developers keep making the same mistakes.