forked from AKHIL-149/vulnerable-node-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
208 lines (191 loc) · 13 KB
/
Copy pathserver.js
File metadata and controls
208 lines (191 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/**
* server.js — Application Entry Point
*
* ─────────────────────────────────────────────────────────────────────────────
* SECURITY CHANGES MADE IN THIS FILE:
* ─────────────────────────────────────────────────────────────────────────────
*
* FIX 6 — Added Helmet.js for HTTP Security Headers ← MEDIUM
*
* BEFORE: No security headers were set. The server responded with Express's
* default headers, which include:
* X-Powered-By: Express
* and are MISSING critical browser security headers entirely.
*
* AFTER: helmet() is added as the FIRST middleware. It sets ~12 security
* headers automatically in a single line. Each header is explained
* individually below in the "What Helmet adds" section.
*
* EXTRA FIX — Removed information leakage: X-Powered-By header
* BEFORE: Express automatically sends "X-Powered-By: Express" on every
* response. This tells attackers exactly what framework you are
* using, helping them target known Express vulnerabilities.
* AFTER: helmet() disables this header automatically. No extra code needed.
*
* EXTRA FIX — Global error handler added
* BEFORE: Unhandled errors propagated with Express's default handler, which
* in development mode sends the full stack trace to the client —
* leaking file paths, library names, and code structure.
* AFTER: A global error handler catches all errors, logs them server-side,
* and returns only a generic message to the client.
*
* EXTRA FIX — JSON body size limit added
* BEFORE: express.json() accepted bodies of unlimited size. An attacker
* could send a gigabyte-sized JSON payload to exhaust server memory
* (a simple Denial of Service attack).
* AFTER: express.json({ limit: '10kb' }) rejects anything larger than 10kb
* — sufficient for all legitimate note content.
*
* NOTE — Rate limiter scope corrected
* BEFORE: loginLimiter was applied only to /auth/login.
* The /auth/register endpoint was unprotected — an attacker could
* create thousands of accounts programmatically with no throttling.
* AFTER: The rate limiter is applied to the entire /auth prefix,
* covering both /auth/login and /auth/register.
*
* ─────────────────────────────────────────────────────────────────────────────
* WHAT HELMET ADDS — Headers set by helmet() and why each matters:
* ─────────────────────────────────────────────────────────────────────────────
*
* 1. X-Content-Type-Options: nosniff
* Prevents the browser from "sniffing" the content type of a response.
* Without this: a browser might execute a .jpg file as JavaScript if it
* looks like a script — a technique used in some upload-based XSS attacks.
* With this: the browser always uses the declared Content-Type and never
* guesses or overrides it.
*
* 2. X-Frame-Options: SAMEORIGIN
* Prevents your app from being embedded inside an <iframe> on another site.
* Without this: an attacker can put your login page inside an invisible
* iframe overlaid on their site. When a user "clicks" on the attacker's
* page, they are actually clicking on your login form — a clickjacking attack.
* With this: browsers refuse to render your app inside any foreign iframe.
*
* 3. Strict-Transport-Security (HSTS): max-age=15552000
* Tells the browser: "Only ever connect to this site over HTTPS — never HTTP."
* Without this: an attacker on the same network (café WiFi) can intercept
* the user's first HTTP request and downgrade the connection before it
* upgrades to HTTPS (a "SSL stripping" attack).
* With this: after the first visit, the browser refuses to connect over HTTP
* for the next 180 days (15552000 seconds), even if the user types http://.
*
* 4. Content-Security-Policy (CSP)
* Tells the browser which sources of scripts, styles, and media are trusted.
* Without this: even if an XSS payload gets through, the browser executes it.
* With this: the browser blocks any script that didn't come from an approved
* source. This is a powerful second layer of defence against XSS.
* Helmet sets a restrictive default CSP — you may need to adjust it if your
* frontend loads resources from CDNs.
*
* 5. X-XSS-Protection: 0
* Counterintuitively, Helmet disables the browser's built-in XSS filter.
* The old filter (IE/Chrome) had known bypasses and could actually introduce
* vulnerabilities. Modern browsers rely on CSP instead, which is far stronger.
*
* 6. Referrer-Policy: no-referrer
* Controls what URL is sent in the Referer header when a user clicks a link.
* Without this: if a user on your app clicks an external link, the full URL
* (including any tokens or IDs in the path) is sent to the external site.
* With this: no referrer information is leaked to external sites at all.
*
* 7. Permissions-Policy (formerly Feature-Policy)
* Disables browser features your API doesn't need: camera, microphone,
* geolocation, payment APIs, etc. Reduces attack surface if XSS occurs.
*
* 8. X-Powered-By: [removed]
* Express sets "X-Powered-By: Express" by default on every response.
* This tells attackers your framework and version, helping them find
* known exploits. Helmet removes this header entirely.
*
* ─────────────────────────────────────────────────────────────────────────────
*/
require('dotenv').config();
const express = require('express');
const helmet = require('helmet'); // FIX 6
const corsMiddleware = require('./middleware/cors');
const requestLogger = require('./middleware/logger');
const { authLimiter } = require('./middleware/rateLimit');
const app = express();
// ─────────────────────────────────────────────────────────────────────────────
// Security Middleware — ORDER MATTERS
// Helmet and CORS must be first so every response, including errors and 404s,
// gets the correct security headers. Never put route handlers before these.
// ─────────────────────────────────────────────────────────────────────────────
// ── FIX 6: Helmet — sets all HTTP security headers in one line ────────────────
// Must be the VERY FIRST middleware so headers are present on every response,
// including error responses, 404s, and preflight OPTIONS requests.
// See detailed header explanations in the block comment above.
app.use(helmet());
// ── CORS — allow requests only from trusted origins (fixed in middleware/cors.js)
app.use(corsMiddleware);
// ── Body parser — EXTRA FIX: 10kb size limit prevents memory exhaustion DoS ──
// Without a limit, an attacker could POST a 1GB JSON body and crash the server.
app.use(express.json({ limit: '10kb' }));
// ── Request logger — logs method, path, status, IP for audit trail ────────────
app.use(requestLogger);
// ─────────────────────────────────────────────────────────────────────────────
// Rate Limiting
// ─────────────────────────────────────────────────────────────────────────────
// ── NOTE FIX: Apply rate limiter to ALL /auth routes, not just /auth/login ───
// BEFORE: loginLimiter was only on /auth/login
// /auth/register had no rate limiting — attacker could create
// thousands of accounts programmatically with no throttling.
// AFTER: authLimiter covers the entire /auth prefix:
// /auth/login, /auth/register, and any future auth endpoints.
app.use('/auth', authLimiter);
// ─────────────────────────────────────────────────────────────────────────────
// Routes
// ─────────────────────────────────────────────────────────────────────────────
app.use('/auth', require('./routes/auth'));
app.use('/users', require('./routes/users'));
app.use('/notes', require('./routes/notes'));
app.use('/admin', require('./routes/admin'));
app.use('/export', require('./routes/export'));
// ── Health check — no auth required, safe to expose publicly ─────────────────
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ─────────────────────────────────────────────────────────────────────────────
// 404 Handler
// Catches any request that didn't match a route above.
// Must be defined AFTER all routes.
// ─────────────────────────────────────────────────────────────────────────────
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// ─────────────────────────────────────────────────────────────────────────────
// Global Error Handler — EXTRA FIX: prevent stack trace leakage
//
// Express calls this whenever a route handler passes an error to next(err)
// or throws inside an async handler (with express-async-errors or try/catch).
//
// BEFORE: No global error handler. Express's default handler in development
// sends the full stack trace to the client, leaking:
// - Your file paths (e.g. /home/ubuntu/app/routes/notes.js:42)
// - Library names and versions
// - Internal error messages from SQLite
//
// AFTER: Stack trace is logged server-side (for debugging).
// Only a generic message is sent to the client.
//
// Must be defined LAST — after all routes and the 404 handler.
// Express identifies error handlers by their 4-parameter signature (err, req, res, next).
// ─────────────────────────────────────────────────────────────────────────────
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
// Log the full error server-side so developers can debug it
console.error(`[ERROR] ${req.method} ${req.path} —`, err.message);
console.error(err.stack);
// Send only a generic message to the client — never the stack trace or err.message
// err.status allows route handlers to set a specific status: next(Object.assign(new Error('...'), { status: 400 }))
const status = err.status || 500;
res.status(status).json({ error: 'Internal server error' });
});
// ─────────────────────────────────────────────────────────────────────────────
// Start Server
// ─────────────────────────────────────────────────────────────────────────────
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`[server] Running on port ${PORT} (${process.env.NODE_ENV || 'development'})`);
});
module.exports = app; // exported for testing