From bacaf257c3e4b563f7be7f9bdf140a73899324bf Mon Sep 17 00:00:00 2001 From: vasu kamani Date: Fri, 27 Feb 2026 13:21:55 +0530 Subject: [PATCH 1/2] Implement comprehensive input sanitization and security features --- README.md | 57 +++ src/app.js | 18 +- src/controllers/auth.controller.js | 56 +++ src/controllers/challenge.controller.js | 61 +++ src/controllers/leetcode.controller.js | 34 +- src/middlewares/sanitization.middleware.js | 194 ++++++++ src/routes/auth.routes.js | 7 +- src/routes/challenge.routes.js | 1 + src/utils/sanitizer.js | 499 +++++++++++++++++++++ 9 files changed, 922 insertions(+), 5 deletions(-) create mode 100644 src/middlewares/sanitization.middleware.js create mode 100644 src/utils/sanitizer.js diff --git a/README.md b/README.md index 30eb359..ace1a9a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,48 @@ A production-ready backend application for tracking LeetCode daily challenges wi - **Penalty System**: Virtual penalty tracking for missed days - **Dashboard**: Comprehensive progress overview and leaderboards - **Clean Architecture**: Service-based structure with separation of concerns +- **🔒 Security Features**: Centralized input sanitization and validation (see [SECURITY.md](SECURITY.md)) + +## 🔒 Security Features + +This application implements comprehensive security measures to protect against common web vulnerabilities: + +### Protected Against + +✅ **Cross-Site Scripting (XSS)** - Script tag removal and protocol validation +✅ **HTML/Script Injection** - Event handler stripping and tag sanitization +✅ **Path Traversal** - Directory traversal detection and blocking +✅ **Malicious Protocol Injection** - JavaScript/data protocol blocking +✅ **Control Character Injection** - Null byte and control character removal +✅ **DoS via Large Inputs** - Request size limits and length enforcement +✅ **SQL Injection Detection** - Pattern detection and logging + +### Security Implementation + +- **Centralized Sanitization Utility** ([src/utils/sanitizer.js](src/utils/sanitizer.js)) + - Specialized sanitizers for email, username, URL, filename, JSON + - Security threat detection and scanning + - Configurable length limits (10KB-100KB) + +- **Global Security Middleware** ([src/middlewares/sanitization.middleware.js](src/middlewares/sanitization.middleware.js)) + - Automatic input sanitization for body, query, and params + - Real-time security scanning with threat blocking + - Request payload size enforcement (100KB limit) + +- **Controller-Level Field Sanitization** + - Field-specific validation rules + - Type-safe sanitization + - Express-validator integration + +### Testing Security + +Run the security test suite: + +```bash +node test-sanitization.js +``` + +For detailed security documentation, see **[SECURITY.md](SECURITY.md)** ## 🛠️ Tech Stack @@ -48,6 +90,21 @@ src/ │ ├── auth.service.js # Authentication business logic │ ├── challenge.service.js # Challenge business logic │ ├── leetcode.service.js # LeetCode API integration + │ ├── penalty.service.js # Penalty management + │ └── evaluation.service.js # Daily evaluation logic + ├── middlewares/ + │ ├── auth.middleware.js # JWT authentication + │ ├── error.middleware.js # Error handling + │ ├── rateLimiter.middleware.js # Rate limiting + │ └── sanitization.middleware.js # 🔒 Input sanitization + ├── utils/ + │ ├── jwt.js # JWT utilities + │ ├── encryption.js # Encryption utilities + │ ├── logger.js # Winston logger + │ └── sanitizer.js # 🔒 Security sanitizer + └── prisma/ + └── schema.prisma # Database schema +``` │ ├── penalty.service.js # Penalty management │ └── evaluation.service.js # Daily evaluation logic ├── middlewares/ diff --git a/src/app.js b/src/app.js index 45ac8c4..b3caf9c 100644 --- a/src/app.js +++ b/src/app.js @@ -2,7 +2,6 @@ const express = require("express"); const cors = require("cors"); const { config } = require("./config/env"); const { errorHandler, notFound } = require("./middlewares/error.middleware"); -const logger = require("./utils/logger"); // Import routes const authRoutes = require("./routes/auth.routes"); @@ -11,6 +10,13 @@ const dashboardRoutes = require("./routes/dashboard.routes"); const leetcodeRoutes = require("./routes/leetcode.routes"); const { apiLimiter, authLimiter } = require('./middlewares/rateLimiter.middleware'); +// Import security middlewares +const { + sanitizeInputs, + securityScanMiddleware, + enforceSizeLimit, +} = require("./middlewares/sanitization.middleware"); + /** * Initialize Express application */ @@ -36,6 +42,16 @@ const createApp = () => { app.use(express.json()); app.use(express.urlencoded({ extended: true })); + // 4. Input Sanitization & Security Middlewares (Team T066 Security Implementation) + // Enforce maximum request size to prevent DoS attacks + app.use(enforceSizeLimit(100000)); // 100KB max payload + + // Perform security scanning to detect XSS, SQL injection, path traversal, etc. + app.use(securityScanMiddleware); + + // Sanitize all inputs in body, query params, and URL params + app.use(sanitizeInputs({ maxLength: 1000 })); + // Request logging middleware app.use((req, res, next) => { // logger.info(`${req.method} ${req.path}`); diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index cd52a0b..0970139 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,11 +1,24 @@ const authService = require("../services/auth.service"); const { asyncHandler } = require("../middlewares/error.middleware"); const { body, validationResult } = require("express-validator"); +const { sanitizeFields } = require("../middlewares/sanitization.middleware"); +const { + sanitizeEmail, + sanitizeUsername, + sanitizePassword, +} = require("../utils/sanitizer"); /** * Validation middleware for registration */ const validateRegister = [ + // Sanitize fields before validation + sanitizeFields({ + email: { type: "email", required: true }, + username: { type: "username", required: true }, + password: { type: "password", required: true }, + leetcodeUsername: { type: "username", required: false }, + }), body("email") .isEmail() .normalizeEmail() @@ -29,6 +42,15 @@ const validateRegister = [ * Validation middleware for login */ const validateLogin = [ + // Sanitize fields before validation + sanitizeFields({ + emailOrUsername: { + type: "string", + required: true, + options: { maxLength: 254 }, + }, + password: { type: "password", required: true }, + }), body("emailOrUsername") .notEmpty() .withMessage("Email or username is required"), @@ -105,11 +127,44 @@ const getProfile = asyncHandler(async (req, res) => { }); }); +/** + * Validation middleware for profile update + */ +const validateUpdateProfile = [ + sanitizeFields({ + leetcodeUsername: { type: "username", required: false }, + currentPassword: { type: "password", required: false }, + newPassword: { type: "password", required: false }, + }), + body("leetcodeUsername") + .optional() + .isLength({ min: 1, max: 50 }) + .withMessage("LeetCode username must be 1-50 characters"), + body("currentPassword") + .optional() + .isLength({ min: 6 }) + .withMessage("Current password must be at least 6 characters"), + body("newPassword") + .optional() + .isLength({ min: 6 }) + .withMessage("New password must be at least 6 characters"), +]; + /** * Update user profile * PUT /api/auth/profile */ const updateProfile = asyncHandler(async (req, res) => { + // Check for validation errors + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: "Validation failed", + errors: errors.array(), + }); + } + const { leetcodeUsername, currentPassword, newPassword } = req.body; const user = await authService.updateProfile(req.user.id, { @@ -132,4 +187,5 @@ module.exports = { updateProfile, validateRegister, validateLogin, + validateUpdateProfile, }; diff --git a/src/controllers/challenge.controller.js b/src/controllers/challenge.controller.js index 871ae2e..1782541 100644 --- a/src/controllers/challenge.controller.js +++ b/src/controllers/challenge.controller.js @@ -1,11 +1,58 @@ const challengeService = require("../services/challenge.service"); const { asyncHandler } = require("../middlewares/error.middleware"); const { body, validationResult } = require("express-validator"); +const { sanitizeFields } = require("../middlewares/sanitization.middleware"); +const { sanitizeString } = require("../utils/sanitizer"); /** * Validation middleware for creating challenge */ const validateCreateChallenge = [ + // Sanitize fields before validation + sanitizeFields({ + name: { + type: "string", + required: true, + options: { maxLength: 100 }, + }, + description: { + type: "text", + required: false, + options: { maxLength: 500 }, + }, + minSubmissionsPerDay: { + type: "number", + required: false, + }, + difficultyFilter: { + type: "array", + required: true, + itemType: "string", + }, + uniqueProblemConstraint: { + type: "boolean", + required: false, + }, + penaltyAmount: { + type: "number", + required: false, + }, + visibility: { + type: "string", + required: false, + options: { maxLength: 20 }, + }, + startDate: { + type: "string", + required: true, + options: { maxLength: 50 }, + }, + endDate: { + type: "string", + required: true, + options: { maxLength: 50 }, + }, + }), body("name") .isLength({ min: 3, max: 100 }) .withMessage("Challenge name must be 3-100 characters"), @@ -119,6 +166,19 @@ const getUserChallenges = asyncHandler(async (req, res) => { }); }); +/** + * Validation middleware for status update + */ +const validateStatusUpdate = [ + sanitizeFields({ + status: { + type: "string", + required: true, + options: { maxLength: 20 }, + }, + }), +]; + /** * Update challenge status * PATCH /api/challenges/:id/status @@ -154,4 +214,5 @@ module.exports = { getUserChallenges, updateChallengeStatus, validateCreateChallenge, + validateStatusUpdate, }; diff --git a/src/controllers/leetcode.controller.js b/src/controllers/leetcode.controller.js index 42dfc3c..4555786 100644 --- a/src/controllers/leetcode.controller.js +++ b/src/controllers/leetcode.controller.js @@ -1,12 +1,22 @@ const leetcodeService = require("../services/leetcode.service"); const { asyncHandler } = require("../middlewares/error.middleware"); +const { sanitizeUsername } = require("../utils/sanitizer"); /** * Fetch user's LeetCode profile * GET /api/leetcode/profile/:username */ const getUserProfile = asyncHandler(async (req, res) => { - const { username } = req.params; + // Sanitize username param + const username = sanitizeUsername(req.params.username); + + if (!username) { + return res.status(400).json({ + success: false, + message: "Invalid username format", + }); + } + const profile = await leetcodeService.fetchUserProfile(username); res.status(200).json({ @@ -20,7 +30,15 @@ const getUserProfile = asyncHandler(async (req, res) => { * GET /api/leetcode/test/:username */ const testConnection = asyncHandler(async (req, res) => { - const { username } = req.params; + // Sanitize username param + const username = sanitizeUsername(req.params.username); + + if (!username) { + return res.status(400).json({ + success: false, + message: "Invalid username format", + }); + } // Fetch recent submissions const today = new Date(); @@ -48,7 +66,17 @@ const testConnection = asyncHandler(async (req, res) => { * GET /api/leetcode/problem/:titleSlug */ const getProblemMetadata = asyncHandler(async (req, res) => { - const { titleSlug } = req.params; + // Sanitize titleSlug param - allow alphanumeric, hyphens, underscores + const { sanitizeString } = require("../utils/sanitizer"); + const titleSlug = sanitizeString(req.params.titleSlug, { maxLength: 200 }); + + if (!titleSlug || !/^[a-zA-Z0-9-_]+$/.test(titleSlug)) { + return res.status(400).json({ + success: false, + message: "Invalid problem title slug format", + }); + } + const metadata = await leetcodeService.fetchProblemMetadata(titleSlug); if (!metadata) { diff --git a/src/middlewares/sanitization.middleware.js b/src/middlewares/sanitization.middleware.js new file mode 100644 index 0000000..7cd6968 --- /dev/null +++ b/src/middlewares/sanitization.middleware.js @@ -0,0 +1,194 @@ +/** + * Sanitization Middleware + * Provides middleware functions for automatic input sanitization and security scanning + */ + +const { + sanitizeString, + sanitizeJson, + securityScan, +} = require("../utils/sanitizer"); +const logger = require("../utils/logger"); + +/** + * Middleware to sanitize all string inputs in request body, query, and params + * @param {Object} options - Configuration options + */ +const sanitizeInputs = (options = {}) => { + return (req, res, next) => { + try { + // Sanitize request body + if (req.body && typeof req.body === "object") { + req.body = sanitizeJson(req.body, { + maxLength: options.maxLength || 1000, + }); + } + + // Sanitize query parameters + if (req.query && typeof req.query === "object") { + for (const key in req.query) { + if (typeof req.query[key] === "string") { + req.query[key] = sanitizeString(req.query[key], { + maxLength: 500, + }); + } + } + } + + // Sanitize URL parameters + if (req.params && typeof req.params === "object") { + for (const key in req.params) { + if (typeof req.params[key] === "string") { + req.params[key] = sanitizeString(req.params[key], { + maxLength: 200, + }); + } + } + } + + next(); + } catch (error) { + logger.error(`Sanitization error: ${error.message}`); + return res.status(400).json({ + success: false, + message: "Invalid input detected", + error: error.message, + }); + } + }; +}; + +/** + * Middleware to perform security scanning on inputs + * Blocks requests with detected threats + */ +const securityScanMiddleware = (req, res, next) => { + const scanInputs = (obj, path = "") => { + if (!obj) return { safe: true, threats: [] }; + + if (typeof obj === "string") { + const scan = securityScan(obj); + if (!scan.safe) { + return { + safe: false, + threats: scan.threats, + path: path || "input", + }; + } + } else if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + const result = scanInputs(obj[i], `${path}[${i}]`); + if (!result.safe) return result; + } + } else if (typeof obj === "object") { + for (const key in obj) { + const result = scanInputs(obj[key], path ? `${path}.${key}` : key); + if (!result.safe) return result; + } + } + + return { safe: true, threats: [] }; + }; + + try { + // Scan body + const bodyResult = scanInputs(req.body, "body"); + if (!bodyResult.safe) { + logger.warn( + `Security threat detected in ${bodyResult.path}: ${bodyResult.threats.join(", ")}` + ); + return res.status(400).json({ + success: false, + message: "Security threat detected in input", + threats: bodyResult.threats, + }); + } + + // Scan query + const queryResult = scanInputs(req.query, "query"); + if (!queryResult.safe) { + logger.warn( + `Security threat detected in ${queryResult.path}: ${queryResult.threats.join(", ")}` + ); + return res.status(400).json({ + success: false, + message: "Security threat detected in input", + threats: queryResult.threats, + }); + } + + // Scan params + const paramsResult = scanInputs(req.params, "params"); + if (!paramsResult.safe) { + logger.warn( + `Security threat detected in ${paramsResult.path}: ${paramsResult.threats.join(", ")}` + ); + return res.status(400).json({ + success: false, + message: "Security threat detected in input", + threats: paramsResult.threats, + }); + } + + next(); + } catch (error) { + logger.error(`Security scan error: ${error.message}`); + return res.status(500).json({ + success: false, + message: "Security scanning failed", + }); + } +}; + +/** + * Middleware to enforce content length limits + * @param {number} maxSize - Maximum content length in bytes + */ +const enforceSizeLimit = (maxSize = 100000) => { + return (req, res, next) => { + const contentLength = req.headers["content-length"]; + + if (contentLength && parseInt(contentLength) > maxSize) { + logger.warn( + `Request rejected: content length ${contentLength} exceeds limit ${maxSize}` + ); + return res.status(413).json({ + success: false, + message: "Request payload too large", + }); + } + + next(); + }; +}; + +/** + * Middleware to sanitize and validate specific fields in request body + * @param {Object} fieldRules - Rules for each field + */ +const sanitizeFields = (fieldRules) => { + return (req, res, next) => { + if (!req.body || typeof req.body !== "object") { + return next(); + } + + try { + const { sanitizeObject } = require("../utils/sanitizer"); + req.body = sanitizeObject(req.body, fieldRules); + next(); + } catch (error) { + logger.error(`Field sanitization error: ${error.message}`); + return res.status(400).json({ + success: false, + message: error.message, + }); + } + }; +}; + +module.exports = { + sanitizeInputs, + securityScanMiddleware, + enforceSizeLimit, + sanitizeFields, +}; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index f7ace29..8853e1f 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -33,6 +33,11 @@ router.get("/profile", authenticate, authController.getProfile); * @desc Update user profile * @access Private */ -router.put("/profile", authenticate, authController.updateProfile); +router.put( + "/profile", + authenticate, + authController.validateUpdateProfile, + authController.updateProfile +); module.exports = router; diff --git a/src/routes/challenge.routes.js b/src/routes/challenge.routes.js index 5f5d741..ce5b78e 100644 --- a/src/routes/challenge.routes.js +++ b/src/routes/challenge.routes.js @@ -44,6 +44,7 @@ router.post("/:id/join", authenticate, challengeController.joinChallenge); router.patch( "/:id/status", authenticate, + challengeController.validateStatusUpdate, challengeController.updateChallengeStatus ); diff --git a/src/utils/sanitizer.js b/src/utils/sanitizer.js new file mode 100644 index 0000000..4acc9be --- /dev/null +++ b/src/utils/sanitizer.js @@ -0,0 +1,499 @@ +/** + * Input Sanitization Utility + * Centralizes all input sanitization and validation logic to protect against: + * - XSS (Cross-Site Scripting) + * - HTML/Script injection + * - Path traversal attacks + * - Malicious protocol injection + * - Control character injection + * - DoS via extremely long inputs + * - JSON payload injection + */ + +const logger = require("./logger"); + +// Configuration constants +const CONFIG = { + MAX_STRING_LENGTH: 10000, // 10KB for general strings + MAX_TEXT_LENGTH: 50000, // 50KB for long text content + MAX_URL_LENGTH: 2048, + MAX_EMAIL_LENGTH: 254, + MAX_USERNAME_LENGTH: 50, + MAX_PASSWORD_LENGTH: 128, + MAX_FILENAME_LENGTH: 255, + MAX_JSON_LENGTH: 100000, // 100KB for JSON payloads +}; + +// Dangerous patterns to detect and block +const DANGEROUS_PATTERNS = { + // XSS and script injection + SCRIPT_TAGS: /)<[^<]*)*<\/script>/gi, + HTML_TAGS: /<[^>]*>/g, + EVENT_HANDLERS: /on\w+\s*=\s*["'][^"']*["']/gi, + + // Protocol injection + JAVASCRIPT_PROTOCOL: /^\s*javascript:/i, + DATA_PROTOCOL: /^\s*data:/i, + VBSCRIPT_PROTOCOL: /^\s*vbscript:/i, + FILE_PROTOCOL: /^\s*file:/i, + + // Path traversal + PATH_TRAVERSAL: /(\.\.[\/\\]|\.\.%2[fF]|\.\.%5[cC])/, + + // Control characters (except common whitespace) + CONTROL_CHARS: /[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, + + // SQL injection patterns (basic detection) + SQL_INJECTION: /(union\s+select|insert\s+into|delete\s+from|drop\s+table|update\s+set|or\s+1\s*=\s*1|;\s*--)/i, + + // Null byte injection + NULL_BYTE: /\x00/g, +}; + +// Safe protocols for URLs +const SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"]; + +/** + * Sanitize a general string input + * @param {string} input - The input string to sanitize + * @param {Object} options - Sanitization options + * @returns {string} Sanitized string + */ +function sanitizeString(input, options = {}) { + if (input === null || input === undefined) { + return options.defaultValue || ""; + } + + // Convert to string + let sanitized = String(input); + + // Check length limits + const maxLength = options.maxLength || CONFIG.MAX_STRING_LENGTH; + if (sanitized.length > maxLength) { + logger.warn(`String exceeds max length: ${sanitized.length} > ${maxLength}`); + sanitized = sanitized.substring(0, maxLength); + } + + // Remove control characters (except newlines and tabs if allowed) + if (!options.allowControlChars) { + if (options.allowNewlines) { + // Keep \n and \r\n, remove other control chars + sanitized = sanitized.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, ""); + } else { + // Remove all control characters including newlines + sanitized = sanitized.replace(DANGEROUS_PATTERNS.CONTROL_CHARS, ""); + } + } + + // Remove null bytes + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + + // Remove HTML/Script tags if not explicitly allowed + if (!options.allowHtml) { + sanitized = sanitized.replace(DANGEROUS_PATTERNS.SCRIPT_TAGS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.HTML_TAGS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.EVENT_HANDLERS, ""); + } + + // Trim whitespace unless specified otherwise + if (!options.preserveWhitespace) { + sanitized = sanitized.trim(); + } + + return sanitized; +} + +/** + * Sanitize text content (allows newlines, longer length) + * @param {string} input - The text content + * @param {Object} options - Sanitization options + * @returns {string} Sanitized text + */ +function sanitizeText(input, options = {}) { + return sanitizeString(input, { + ...options, + maxLength: options.maxLength || CONFIG.MAX_TEXT_LENGTH, + allowNewlines: true, + }); +} + +/** + * Sanitize email address + * @param {string} email - Email address + * @returns {string} Sanitized email + */ +function sanitizeEmail(email) { + if (!email) return ""; + + let sanitized = String(email).toLowerCase().trim(); + + // Length check + if (sanitized.length > CONFIG.MAX_EMAIL_LENGTH) { + logger.warn(`Email exceeds max length: ${sanitized.length}`); + throw new Error("Email address is too long"); + } + + // Remove control characters + sanitized = sanitized.replace(DANGEROUS_PATTERNS.CONTROL_CHARS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + + // Remove any HTML tags or scripts + sanitized = sanitized.replace(DANGEROUS_PATTERNS.SCRIPT_TAGS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.HTML_TAGS, ""); + + // Basic email format validation + const emailRegex = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; + + if (!emailRegex.test(sanitized)) { + throw new Error("Invalid email format"); + } + + return sanitized; +} + +/** + * Sanitize username + * @param {string} username - Username + * @returns {string} Sanitized username + */ +function sanitizeUsername(username) { + if (!username) return ""; + + let sanitized = String(username).trim(); + + // Length check + if (sanitized.length > CONFIG.MAX_USERNAME_LENGTH) { + logger.warn(`Username exceeds max length: ${sanitized.length}`); + throw new Error("Username is too long"); + } + + // Remove control characters + sanitized = sanitized.replace(DANGEROUS_PATTERNS.CONTROL_CHARS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + + // Remove any HTML tags or scripts + sanitized = sanitized.replace(DANGEROUS_PATTERNS.SCRIPT_TAGS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.HTML_TAGS, ""); + + // Alphanumeric, underscore, and hyphen only + sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, ""); + + return sanitized; +} + +/** + * Sanitize password (minimal sanitization, preserve special chars) + * @param {string} password - Password + * @returns {string} Sanitized password + */ +function sanitizePassword(password) { + if (!password) return ""; + + let sanitized = String(password); + + // Length check + if (sanitized.length > CONFIG.MAX_PASSWORD_LENGTH) { + logger.warn(`Password exceeds max length: ${sanitized.length}`); + throw new Error("Password is too long"); + } + + // Remove only null bytes and control chars (preserve special chars for password strength) + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, ""); + + return sanitized; +} + +/** + * Sanitize URL + * @param {string} url - URL to sanitize + * @returns {string} Sanitized URL + */ +function sanitizeUrl(url) { + if (!url) return ""; + + let sanitized = String(url).trim(); + + // Length check + if (sanitized.length > CONFIG.MAX_URL_LENGTH) { + logger.warn(`URL exceeds max length: ${sanitized.length}`); + throw new Error("URL is too long"); + } + + // Remove control characters + sanitized = sanitized.replace(DANGEROUS_PATTERNS.CONTROL_CHARS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + + // Check for malicious protocols + if ( + DANGEROUS_PATTERNS.JAVASCRIPT_PROTOCOL.test(sanitized) || + DANGEROUS_PATTERNS.DATA_PROTOCOL.test(sanitized) || + DANGEROUS_PATTERNS.VBSCRIPT_PROTOCOL.test(sanitized) || + DANGEROUS_PATTERNS.FILE_PROTOCOL.test(sanitized) + ) { + logger.warn(`Malicious protocol detected in URL: ${sanitized.substring(0, 50)}`); + throw new Error("Invalid URL protocol"); + } + + // Validate protocol is safe + try { + const urlObj = new URL(sanitized); + if (!SAFE_PROTOCOLS.includes(urlObj.protocol)) { + throw new Error("Unsafe URL protocol"); + } + } catch (error) { + logger.warn(`Invalid URL format: ${error.message}`); + throw new Error("Invalid URL format"); + } + + return sanitized; +} + +/** + * Sanitize filename + * @param {string} filename - Filename to sanitize + * @returns {string} Sanitized filename + */ +function sanitizeFilename(filename) { + if (!filename) return ""; + + let sanitized = String(filename).trim(); + + // Length check + if (sanitized.length > CONFIG.MAX_FILENAME_LENGTH) { + logger.warn(`Filename exceeds max length: ${sanitized.length}`); + throw new Error("Filename is too long"); + } + + // Remove control characters + sanitized = sanitized.replace(DANGEROUS_PATTERNS.CONTROL_CHARS, ""); + sanitized = sanitized.replace(DANGEROUS_PATTERNS.NULL_BYTE, ""); + + // Check for path traversal + if (DANGEROUS_PATTERNS.PATH_TRAVERSAL.test(sanitized)) { + logger.warn(`Path traversal detected in filename: ${sanitized}`); + throw new Error("Invalid filename: path traversal detected"); + } + + // Remove potentially dangerous characters + // Allow alphanumeric, spaces, dots, hyphens, underscores + sanitized = sanitized.replace(/[^a-zA-Z0-9.\-_ ]/g, ""); + + // Prevent multiple dots (could be used for directory traversal) + sanitized = sanitized.replace(/\.{2,}/g, "."); + + // Prevent starting with dot (hidden files) + sanitized = sanitized.replace(/^\.+/, ""); + + return sanitized; +} + +/** + * Sanitize JSON payload + * @param {Object} json - JSON object to sanitize + * @param {Object} options - Sanitization options + * @returns {Object} Sanitized JSON object + */ +function sanitizeJson(json, options = {}) { + if (json === null || json === undefined) { + return options.defaultValue || {}; + } + + // Convert to string to check size + const jsonString = JSON.stringify(json); + if (jsonString.length > CONFIG.MAX_JSON_LENGTH) { + logger.warn(`JSON payload exceeds max length: ${jsonString.length}`); + throw new Error("JSON payload is too large"); + } + + // Recursively sanitize all string values + const sanitizeValue = (value) => { + if (typeof value === "string") { + return sanitizeString(value, options); + } else if (Array.isArray(value)) { + return value.map(sanitizeValue); + } else if (typeof value === "object" && value !== null) { + const sanitized = {}; + for (const key in value) { + // Sanitize keys as well + const sanitizedKey = sanitizeString(key, { maxLength: 100 }); + sanitized[sanitizedKey] = sanitizeValue(value[key]); + } + return sanitized; + } + return value; + }; + + return sanitizeValue(json); +} + +/** + * Sanitize object with specific field rules + * @param {Object} obj - Object to sanitize + * @param {Object} rules - Sanitization rules for each field + * @returns {Object} Sanitized object + */ +function sanitizeObject(obj, rules) { + if (!obj || typeof obj !== "object") { + return {}; + } + + const sanitized = {}; + + for (const [field, rule] of Object.entries(rules)) { + if (!(field in obj)) { + // Field not provided, use default if specified + if (rule.required) { + throw new Error(`Required field missing: ${field}`); + } + if (rule.default !== undefined) { + sanitized[field] = rule.default; + } + continue; + } + + const value = obj[field]; + + try { + switch (rule.type) { + case "string": + sanitized[field] = sanitizeString(value, rule.options || {}); + break; + case "text": + sanitized[field] = sanitizeText(value, rule.options || {}); + break; + case "email": + sanitized[field] = sanitizeEmail(value); + break; + case "username": + sanitized[field] = sanitizeUsername(value); + break; + case "password": + sanitized[field] = sanitizePassword(value); + break; + case "url": + sanitized[field] = sanitizeUrl(value); + break; + case "filename": + sanitized[field] = sanitizeFilename(value); + break; + case "number": + sanitized[field] = Number(value); + if (isNaN(sanitized[field])) { + throw new Error(`Invalid number: ${field}`); + } + break; + case "boolean": + sanitized[field] = Boolean(value); + break; + case "array": + if (!Array.isArray(value)) { + throw new Error(`Expected array for field: ${field}`); + } + sanitized[field] = value.map((item) => { + if (rule.itemType) { + return sanitizeString(item, rule.options || {}); + } + return item; + }); + break; + case "json": + sanitized[field] = sanitizeJson(value, rule.options || {}); + break; + default: + sanitized[field] = value; + } + } catch (error) { + logger.error(`Sanitization error for field ${field}: ${error.message}`); + throw new Error(`Invalid input for field ${field}: ${error.message}`); + } + } + + return sanitized; +} + +/** + * Detect potential SQL injection attempts + * @param {string} input - Input to check + * @returns {boolean} True if potential SQL injection detected + */ +function detectSqlInjection(input) { + if (typeof input !== "string") return false; + return DANGEROUS_PATTERNS.SQL_INJECTION.test(input); +} + +/** + * Detect potential XSS attempts + * @param {string} input - Input to check + * @returns {boolean} True if potential XSS detected + */ +function detectXss(input) { + if (typeof input !== "string") return false; + + return ( + DANGEROUS_PATTERNS.SCRIPT_TAGS.test(input) || + DANGEROUS_PATTERNS.EVENT_HANDLERS.test(input) || + DANGEROUS_PATTERNS.JAVASCRIPT_PROTOCOL.test(input) + ); +} + +/** + * Detect potential path traversal attempts + * @param {string} input - Input to check + * @returns {boolean} True if potential path traversal detected + */ +function detectPathTraversal(input) { + if (typeof input !== "string") return false; + return DANGEROUS_PATTERNS.PATH_TRAVERSAL.test(input); +} + +/** + * Comprehensive security scan + * @param {string} input - Input to scan + * @returns {Object} Scan results + */ +function securityScan(input) { + const results = { + safe: true, + threats: [], + }; + + if (detectXss(input)) { + results.safe = false; + results.threats.push("XSS"); + } + + if (detectSqlInjection(input)) { + results.safe = false; + results.threats.push("SQL_INJECTION"); + } + + if (detectPathTraversal(input)) { + results.safe = false; + results.threats.push("PATH_TRAVERSAL"); + } + + if (DANGEROUS_PATTERNS.NULL_BYTE.test(input)) { + results.safe = false; + results.threats.push("NULL_BYTE"); + } + + return results; +} + +module.exports = { + sanitizeString, + sanitizeText, + sanitizeEmail, + sanitizeUsername, + sanitizePassword, + sanitizeUrl, + sanitizeFilename, + sanitizeJson, + sanitizeObject, + detectSqlInjection, + detectXss, + detectPathTraversal, + securityScan, + CONFIG, +}; From bda9d18196216b4b8894987c71fd853cafc0e790 Mon Sep 17 00:00:00 2001 From: vasu kamani Date: Sun, 1 Mar 2026 00:32:25 +0530 Subject: [PATCH 2/2] refactor: remove unused sanitization functions and obsolete validation middleware --- src/controllers/auth.controller.js | 46 ------------------------------ 1 file changed, 46 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index f79348a..353945f 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -2,11 +2,6 @@ const authService = require("../services/auth.service"); const { asyncHandler } = require("../middlewares/error.middleware"); const { body, validationResult } = require("express-validator"); const { sanitizeFields } = require("../middlewares/sanitization.middleware"); -const { - sanitizeEmail, - sanitizeUsername, - sanitizePassword, -} = require("../utils/sanitizer"); /** * Validation middleware for registration @@ -77,47 +72,6 @@ const validateResetPassword = [ .withMessage("New password must be at least 6 characters"), ]; -/** - * Validation middleware for profile update - */ -const validateUpdateProfile = [ - body("leetcodeUsername") - .optional({ nullable: true }) - .isString() - .isLength({ min: 1, max: 50 }) - .withMessage("LeetCode username must be 1-50 characters"), - body("newPassword") - .optional() - .isString() - .isLength({ min: 6 }) - .withMessage("New password must be at least 6 characters"), - body("currentPassword") - .if(body("newPassword").exists({ checkFalsy: true })) - .notEmpty() - .withMessage("Current password is required when setting a new password"), -]; - -/** - * Validation middleware for forgot password - */ -const validateForgotPassword = [ - body("email") - .isEmail() - .normalizeEmail() - .withMessage("Valid email is required"), -]; - -/** - * Validation middleware for reset password - */ -const validateResetPassword = [ - body("token").notEmpty().withMessage("Reset token is required"), - body("newPassword") - .isLength({ min: 6 }) - .withMessage("New password must be at least 6 characters"), -]; - - /** * Register a new user * POST /api/auth/register