Storage & auth security fix

This commit is contained in:
Wayne
2025-07-27 21:01:47 +03:00
parent becd5f1490
commit 898f52ac78
4 changed files with 39 additions and 9 deletions

View File

@@ -19,7 +19,6 @@
"db:migrate:dev": "ts-node-dev src/database/migrate.ts"
},
"dependencies": {
"drizzle-kit": "^0.31.4",
"@aws-sdk/client-s3": "^3.844.0",
"@aws-sdk/lib-storage": "^3.844.0",
"@azure/msal-node": "^3.6.3",
@@ -31,8 +30,10 @@
"cross-fetch": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dotenv": "^17.2.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.2",
"express": "^5.1.0",
"express-rate-limit": "^8.0.1",
"express-validator": "^7.2.1",
"google-auth-library": "^10.1.0",
"googleapis": "^152.0.0",

View File

@@ -1,28 +1,46 @@
import { Request, Response } from 'express';
import { StorageService } from '../../services/StorageService';
import * as path from 'path';
import { storage as storageConfig } from '../../config/storage';
export class StorageController {
constructor(private storageService: StorageService) { }
public downloadFile = async (req: Request, res: Response): Promise<void> => {
const filePath = req.query.path as string;
const unsafePath = req.query.path as string;
if (!filePath) {
if (!unsafePath) {
res.status(400).send('File path is required');
return;
}
// Normalize the path to prevent directory traversal
const normalizedPath = path.normalize(unsafePath).replace(/^(\.\.(\/|\\|$))+/, '');
// Determine the base path from storage configuration
const basePath = storageConfig.type === 'local' ? storageConfig.rootPath : '/';
// Resolve the full path and ensure it's within the storage directory
const fullPath = path.join(basePath, normalizedPath);
if (!fullPath.startsWith(basePath)) {
res.status(400).send('Invalid file path');
return;
}
// Use the sanitized, relative path for storage service operations
const safePath = path.relative(basePath, fullPath);
try {
const fileExists = await this.storageService.exists(filePath);
const fileExists = await this.storageService.exists(safePath);
if (!fileExists) {
console.log(filePath);
res.status(404).send('File not found');
return;
}
const fileStream = await this.storageService.get(filePath);
const fileName = filePath.split('/').pop();
res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);
const fileStream = await this.storageService.get(safePath);
const fileName = path.basename(safePath);
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
fileStream.pipe(res);
} catch (error) {
console.error('Error downloading file:', error);

View File

@@ -0,0 +1,10 @@
import rateLimit from 'express-rate-limit';
// Rate limiter to prevent brute-force attacks on the login endpoint
export const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Limit each IP to 10 login requests per windowMs
message: 'Too many login attempts from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

View File

@@ -1,4 +1,5 @@
import { Router } from 'express';
import { loginRateLimiter } from '../middleware/rateLimiter';
import type { AuthController } from '../controllers/auth.controller';
export const createAuthRouter = (authController: AuthController): Router => {
@@ -9,7 +10,7 @@ export const createAuthRouter = (authController: AuthController): Router => {
* @description Authenticates a user and returns a JWT.
* @access Public
*/
router.post('/login', authController.login);
router.post('/login', loginRateLimiter, authController.login);
return router;
};