Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/check-hardcoded-urls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Check for hardcoded URLs
on:
pull_request:

# Read-only: the job only checks out the tree and greps it.
permissions:
contents: read

jobs:
hardcoded-urls:
runs-on: ubuntu-latest
Expand Down
21 changes: 21 additions & 0 deletions backend/middleware/rateLimit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const rateLimit = require('express-rate-limit');

const WINDOW_MS = 15 * 60 * 1000; // 15 minutes

const common = {
windowMs: WINDOW_MS,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
};

// Broad ceiling for every request the API serves.
const apiLimiter = rateLimit({ ...common, limit: 600 });

// Tighter ceiling for routers that write to storage or fan out to other services.
const writeLimiter = rateLimit({ ...common, limit: 60 });

// Tightest ceiling for upload routes, the most expensive per request.
const uploadLimiter = rateLimit({ ...common, limit: 20 });

module.exports = { apiLimiter, writeLimiter, uploadLimiter };
12 changes: 11 additions & 1 deletion backend/models/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ async function sendMessage(
};

if (file && file.data) {
const filename = Date.now() + '_' + file.name;
// file.name arrives from the client, so collapse it to a bare filename and
// drop anything outside [A-Za-z0-9._-]. That strips directory separators,
// '..' segments and NUL bytes, so the write cannot escape UPLOAD_DIR.
const baseName = path.basename(String(file.name ?? ''));
const safeName = baseName.replace(/[^\w.-]/g, '_').replace(/^\.+/, '') || 'upload';
const filename = Date.now() + '_' + safeName;
const filePath = path.join(UPLOAD_DIR, filename);
// Defence in depth: refuse to write if the resolved path is not directly
// inside UPLOAD_DIR.
if (path.dirname(path.resolve(filePath)) !== path.resolve(UPLOAD_DIR)) {
throw new Error('Refusing to write upload outside of the upload directory');
}
fs.writeFileSync(filePath, Buffer.from(file.data, 'base64'));
msg.file = {
path: '/uploads/' + filename,
Expand Down
13 changes: 13 additions & 0 deletions backend/node_modules/express-rate-limit/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^7.5.1",
"franc": "^6.1.0",
"jsonwebtoken": "^9.0.2",
"twilio": "^4.21.0",
"openai": "^4.76.0"
"openai": "^4.76.0",
"twilio": "^4.21.0"
},
"devDependencies": {
"aws-sdk": "^2.1534.0",
"jest": "^29.7.0"
},
"overrides": {
"qs": "^6.14.1",
"jws": "^3.2.3"
"qs": "^6.14.1",
"jws": "^3.2.3"
},
"jest": {
"testEnvironment": "node"
Expand Down
5 changes: 5 additions & 0 deletions backend/routes/agrotourismRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const multer = require("multer");
const path = require("path");
const Agrotourism = require("../models/agrotourism");
const authMiddleware = require("../middleware/authMiddleware");
const { apiLimiter, uploadLimiter } = require("../middleware/rateLimit");

// Configure Multer for image uploads
const storage = multer.diskStorage({
Expand All @@ -16,6 +17,10 @@ const storage = multer.diskStorage({
});
const upload = multer({ storage });

// Listing reads hit Mongo; /create also accepts up to five image uploads.
router.use(apiLimiter);
router.use("/create", uploadLimiter);

// Create Agrotourism Listing
router.post("/create", authMiddleware, upload.array("images", 5), async (req, res) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions backend/routes/communicationRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ const router = express.Router();
const commController = require('../controllers/communication_controller');
const auth = require('../middleware/authMiddleware');
const asyncHandler = require('../utils/asyncHandler');
const { writeLimiter } = require('../middleware/rateLimit');

router.use(auth);
router.use(writeLimiter);
router.post('/:conversationId', asyncHandler(commController.sendMessage));
router.get('/:conversationId', asyncHandler(commController.listMessages));

Expand Down
2 changes: 2 additions & 0 deletions backend/routes/conversationRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ const router = express.Router();
const auth = require('../middleware/authMiddleware');
const ctrl = require('../controllers/conversation_controller');
const asyncHandler = require('../utils/asyncHandler');
const { writeLimiter } = require('../middleware/rateLimit');

router.use(auth);
router.use(writeLimiter);
router.post('/', asyncHandler(ctrl.create));
router.get('/', asyncHandler(ctrl.list));
router.put('/:id', asyncHandler(ctrl.rename));
Expand Down
5 changes: 4 additions & 1 deletion backend/routes/depositRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware');
const depositController = require('../controllers/deposit_controller');
const asyncHandler = require('../utils/asyncHandler');
const { writeLimiter } = require('../middleware/rateLimit');

// All routes are protected
// All routes are protected and rate limited: every handler moves funds
// or reads transaction history.
router.use(authMiddleware);
router.use(writeLimiter);

// Get or create user deposit account
router.get('/', asyncHandler(depositController.getOrCreateAccount));
Expand Down
4 changes: 4 additions & 0 deletions backend/routes/logRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const {
createTransactionLogItem
} = require('../models/transactionLog');
const authMiddleware = require('../middleware/authMiddleware');
const { writeLimiter } = require('../middleware/rateLimit');

// Both handlers touch DynamoDB, so cap request volume per client.
router.use(writeLimiter);

// Store a new transaction log entry
router.post('/logs', authMiddleware, async (req, res) => {
Expand Down
3 changes: 3 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ try {
}
const path = require("path");
const authMiddleware = require("./middleware/authMiddleware");
const { apiLimiter } = require("./middleware/rateLimit");

// Load environment variables
dotenv.config();
Expand Down Expand Up @@ -129,6 +130,8 @@ global.emitToken = emitToken;
global.emitMessage = emitMessage;

// Middleware
// Global ceiling ahead of auth so unauthenticated floods are shed early.
app.use(apiLimiter);
app.use(authMiddleware);
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

Expand Down
Loading