-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Description
Severity: Medium-High
Component: Express CORS Configuration / Request Handling
Status: Unresolved
Description
While the API is largely unauthenticated (reducing classic CSRF impact), the application accepts preflightless content-types (application/x-www-form-urlencoded, text/plain) on state-changing POST endpoints. This enables cross-site request abuse for resource-intensive operations like repository cloning, potentially leading to denial-of-service or resource exhaustion.
Evidence from Report
From csrf_lowrate_20251114T102308Z.json:
- POST requests with
Content-Type: application/x-www-form-urlencodedaccepted (status 400 due to validation, not CSRF protection) - POST requests with
Content-Type: text/plainaccepted - CORS headers:
Access-Control-Allow-Origin: http://localhost:5173,Access-Control-Allow-Credentials: true
Proof of Concept:
<!-- Attacker's malicious page -->
<form action="http://target-gitray.com/api/repositories" method="POST">
<input type="hidden" name="repoUrl" value="https://github.com/torvalds/linux.git">
<input type="hidden" name="filterOptions" value='{"limit": 10000}'>
</form>
<script>document.forms[0].submit()</script>Impact
- Attackers can force victims to trigger expensive Git clone operations
- Resource exhaustion attacks against the backend infrastructure
- Potential abuse of compute resources and bandwidth
- Could impact service availability for legitimate users
Affected Endpoints
All POST endpoints that trigger resource-intensive operations:
/api/repositories/api/repositories/heatmap/api/repositories/contributors/api/repositories/churn/api/repositories/full-data/api/commits/stream
Recommended Fix
Option 1: Strict Content-Type Enforcement (Recommended)
// apps/backend/src/middleware/csrfProtection.ts
export const strictContentType = (req: Request, res: Response, next: NextFunction) => {
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
const contentType = req.get('Content-Type') || '';
// Only allow application/json
if (!contentType.startsWith('application/json')) {
return res.status(415).json({
error: 'Unsupported Media Type',
code: 'INVALID_CONTENT_TYPE',
message: 'Only application/json is accepted for state-changing operations'
});
}
// Require custom header to prevent simple CSRF
const customHeader = req.get('X-Requested-With');
if (!customHeader) {
return res.status(403).json({
error: 'Forbidden',
code: 'MISSING_CUSTOM_HEADER',
message: 'X-Requested-With header required'
});
}
}
next();
};
// Apply to state-changing routes
app.use('/api/repositories', strictContentType);
app.use('/api/commits/stream', strictContentType);Option 2: Origin/Referer Validation
// apps/backend/src/middleware/csrfProtection.ts
const ALLOWED_ORIGINS = [
'http://localhost:5173',
process.env.FRONTEND_URL
].filter(Boolean);
export const validateOrigin = (req: Request, res: Response, next: NextFunction) => {
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
const origin = req.get('Origin');
const referer = req.get('Referer');
const isValidOrigin = origin && ALLOWED_ORIGINS.includes(origin);
const isValidReferer = referer && ALLOWED_ORIGINS.some(o => referer.startsWith(o));
if (!isValidOrigin && !isValidReferer) {
return res.status(403).json({
error: 'Forbidden',
code: 'INVALID_ORIGIN',
message: 'Request origin not allowed'
});
}
}
next();
};Option 3: CSRF Tokens (Most Secure, More Complex)
import csrf from 'csurf';
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
app.use(csrfProtection);
// Provide token endpoint
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});Testing
# Test 1: Verify form-encoded requests are blocked
curl -X POST http://localhost:3001/api/repositories \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Origin: http://evil.com" \
-d "repoUrl=https://github.com/octocat/Hello-World.git"
# Expected: 415 or 403
# Test 2: Verify text/plain requests are blocked
curl -X POST http://localhost:3001/api/repositories \
-H "Content-Type: text/plain" \
-H "Origin: http://evil.com" \
-d '{"repoUrl":"https://github.com/octocat/Hello-World.git"}'
# Expected: 415 or 403
# Test 3: Verify valid JSON requests still work
curl -X POST http://localhost:3001/api/repositories \
-H "Content-Type: application/json" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Origin: http://localhost:5173" \
-d '{"repoUrl":"https://github.com/octocat/Hello-World.git"}'
# Expected: 200 or validation error (not CSRF error)Metadata
Metadata
Assignees
Labels
No labels