Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/
Expand Down
17 changes: 17 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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
*/
Expand Down Expand Up @@ -49,6 +56,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}`);
Expand Down
40 changes: 40 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
const authService = require("../services/auth.service");
const { asyncHandler } = require("../middlewares/error.middleware");
const { body, validationResult } = require("express-validator");
const { sanitizeFields } = require("../middlewares/sanitization.middleware");

/**
* 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()
Expand All @@ -29,6 +37,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"),
Expand Down Expand Up @@ -166,6 +183,29 @@ 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
Expand Down
61 changes: 61 additions & 0 deletions src/controllers/challenge.controller.js
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -220,6 +280,7 @@ module.exports = {
getUserChallenges,
updateChallengeStatus,
validateCreateChallenge,
validateStatusUpdate,
generateInviteCode,
joinByInviteCode,
validateGenerateInvite,
Expand Down
35 changes: 31 additions & 4 deletions src/controllers/leetcode.controller.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -20,8 +30,15 @@ const getUserProfile = asyncHandler(async (req, res) => {
* GET /api/leetcode/test/:username
*/
const testConnection = asyncHandler(async (req, res) => {
const { username } = req.params;
const { date } = req.query;
// Sanitize username param
const username = sanitizeUsername(req.params.username);

if (!username) {
return res.status(400).json({
success: false,
message: "Invalid username format",
});
}

const targetDate = date ? new Date(date) : new Date();
const submissions = await leetcodeService.fetchSubmissionsForDate(
Expand All @@ -46,7 +63,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) {
Expand Down
Loading