protect against sql injection, xss, csrf
This commit is contained in:
83
backend/middleware/csrf.js
Normal file
83
backend/middleware/csrf.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const csrf = require("csrf");
|
||||
const cookieParser = require("cookie-parser");
|
||||
|
||||
// Initialize CSRF token generator
|
||||
const tokens = new csrf();
|
||||
|
||||
// Generate a secret for signing tokens
|
||||
const secret = tokens.secretSync();
|
||||
|
||||
// CSRF middleware using double submit cookie pattern
|
||||
const csrfProtection = (req, res, next) => {
|
||||
// Skip CSRF for safe methods
|
||||
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Get token from header or body
|
||||
const token =
|
||||
req.headers["x-csrf-token"] || req.body.csrfToken || req.query.csrfToken;
|
||||
|
||||
// Get token from cookie
|
||||
const cookieToken = req.cookies["csrf-token"];
|
||||
|
||||
// Verify both tokens exist and match
|
||||
if (!token || !cookieToken || token !== cookieToken) {
|
||||
return res.status(403).json({
|
||||
error: "Invalid CSRF token",
|
||||
code: "CSRF_TOKEN_MISMATCH",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token is valid
|
||||
if (!tokens.verify(secret, token)) {
|
||||
return res.status(403).json({
|
||||
error: "Invalid CSRF token",
|
||||
code: "CSRF_TOKEN_INVALID",
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Middleware to generate and send CSRF token
|
||||
const generateCSRFToken = (req, res, next) => {
|
||||
const token = tokens.create(secret);
|
||||
|
||||
// Set token in cookie (httpOnly for security)
|
||||
res.cookie("csrf-token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV !== "dev",
|
||||
sameSite: "strict",
|
||||
maxAge: 60 * 60 * 1000, // 1 hour
|
||||
});
|
||||
|
||||
// Also provide token in header for client to use
|
||||
res.set("X-CSRF-Token", token);
|
||||
|
||||
// Make token available to response
|
||||
res.locals.csrfToken = token;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Route to get CSRF token (for initial page loads)
|
||||
const getCSRFToken = (req, res) => {
|
||||
const token = tokens.create(secret);
|
||||
|
||||
res.cookie("csrf-token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV !== "dev",
|
||||
sameSite: "strict",
|
||||
maxAge: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
res.json({ csrfToken: token });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
csrfProtection,
|
||||
generateCSRFToken,
|
||||
getCSRFToken,
|
||||
cookieParser: cookieParser(),
|
||||
};
|
||||
271
backend/middleware/validation.js
Normal file
271
backend/middleware/validation.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const { body, validationResult } = require("express-validator");
|
||||
const DOMPurify = require("dompurify");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
// Create a DOM purify instance for server-side sanitization
|
||||
const window = new JSDOM("").window;
|
||||
const purify = DOMPurify(window);
|
||||
|
||||
// Password strength validation
|
||||
const passwordStrengthRegex =
|
||||
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
|
||||
const commonPasswords = [
|
||||
"password",
|
||||
"123456",
|
||||
"123456789",
|
||||
"qwerty",
|
||||
"abc123",
|
||||
"password123",
|
||||
"admin",
|
||||
"letmein",
|
||||
"welcome",
|
||||
"monkey",
|
||||
"1234567890",
|
||||
];
|
||||
|
||||
// Sanitization middleware
|
||||
const sanitizeInput = (req, res, next) => {
|
||||
const sanitizeValue = (value) => {
|
||||
if (typeof value === "string") {
|
||||
return purify.sanitize(value, { ALLOWED_TAGS: [] });
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const sanitized = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
sanitized[key] = sanitizeValue(val);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
if (req.body) {
|
||||
req.body = sanitizeValue(req.body);
|
||||
}
|
||||
if (req.query) {
|
||||
req.query = sanitizeValue(req.query);
|
||||
}
|
||||
if (req.params) {
|
||||
req.params = sanitizeValue(req.params);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Validation error handler
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
error: "Validation failed",
|
||||
details: errors.array().map((err) => ({
|
||||
field: err.path,
|
||||
message: err.msg,
|
||||
})),
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Registration validation rules
|
||||
const validateRegistration = [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.normalizeEmail()
|
||||
.withMessage("Please provide a valid email address")
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Email must be less than 255 characters"),
|
||||
|
||||
body("password")
|
||||
.isLength({ min: 8, max: 128 })
|
||||
.withMessage("Password must be between 8 and 128 characters")
|
||||
.matches(passwordStrengthRegex)
|
||||
.withMessage(
|
||||
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
|
||||
)
|
||||
.custom((value) => {
|
||||
if (commonPasswords.includes(value.toLowerCase())) {
|
||||
throw new Error(
|
||||
"Password is too common. Please choose a stronger password"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
body("firstName")
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("First name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"First name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("lastName")
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("Last name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"Last name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("username")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 3, max: 30 })
|
||||
.withMessage("Username must be between 3 and 30 characters")
|
||||
.matches(/^[a-zA-Z0-9_-]+$/)
|
||||
.withMessage(
|
||||
"Username can only contain letters, numbers, underscores, and hyphens"
|
||||
),
|
||||
|
||||
body("phone")
|
||||
.optional()
|
||||
.isMobilePhone()
|
||||
.withMessage("Please provide a valid phone number"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Login validation rules
|
||||
const validateLogin = [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.normalizeEmail()
|
||||
.withMessage("Please provide a valid email address"),
|
||||
|
||||
body("password")
|
||||
.notEmpty()
|
||||
.withMessage("Password is required")
|
||||
.isLength({ max: 128 })
|
||||
.withMessage("Password is too long"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Google auth validation
|
||||
const validateGoogleAuth = [
|
||||
body("idToken")
|
||||
.notEmpty()
|
||||
.withMessage("Google ID token is required")
|
||||
.isLength({ max: 2048 })
|
||||
.withMessage("Invalid token format"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Profile update validation
|
||||
const validateProfileUpdate = [
|
||||
body("firstName")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("First name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"First name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("lastName")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("Last name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"Last name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("phone")
|
||||
.optional()
|
||||
.isMobilePhone()
|
||||
.withMessage("Please provide a valid phone number"),
|
||||
|
||||
body("address1")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Address line 1 must be less than 255 characters"),
|
||||
|
||||
body("address2")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Address line 2 must be less than 255 characters"),
|
||||
|
||||
body("city")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("City must be less than 100 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"City can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("state")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("State must be less than 100 characters"),
|
||||
|
||||
body("zipCode")
|
||||
.optional()
|
||||
.trim()
|
||||
.matches(/^[0-9]{5}(-[0-9]{4})?$/)
|
||||
.withMessage("Please provide a valid ZIP code"),
|
||||
|
||||
body("country")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("Country must be less than 100 characters"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Password change validation
|
||||
const validatePasswordChange = [
|
||||
body("currentPassword")
|
||||
.notEmpty()
|
||||
.withMessage("Current password is required"),
|
||||
|
||||
body("newPassword")
|
||||
.isLength({ min: 8, max: 128 })
|
||||
.withMessage("New password must be between 8 and 128 characters")
|
||||
.matches(passwordStrengthRegex)
|
||||
.withMessage(
|
||||
"New password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
|
||||
)
|
||||
.custom((value, { req }) => {
|
||||
if (value === req.body.currentPassword) {
|
||||
throw new Error("New password must be different from current password");
|
||||
}
|
||||
if (commonPasswords.includes(value.toLowerCase())) {
|
||||
throw new Error(
|
||||
"Password is too common. Please choose a stronger password"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
body("confirmPassword").custom((value, { req }) => {
|
||||
if (value !== req.body.newPassword) {
|
||||
throw new Error("Password confirmation does not match");
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
sanitizeInput,
|
||||
handleValidationErrors,
|
||||
validateRegistration,
|
||||
validateLogin,
|
||||
validateGoogleAuth,
|
||||
validateProfileUpdate,
|
||||
validatePasswordChange,
|
||||
};
|
||||
Reference in New Issue
Block a user