-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathserver.js
More file actions
216 lines (193 loc) · 8.73 KB
/
Copy pathserver.js
File metadata and controls
216 lines (193 loc) · 8.73 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
209
210
211
212
213
214
215
216
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const path = require('path');
const fs = require('fs');
const db = require('./database');
const { requireAuth } = require('./middleware/auth');
const app = express();
// Bug fix: on Railway (and Heroku/Render/most PaaS), TLS is terminated at
// the platform's edge proxy — the connection between that proxy and this
// container is plain HTTP. Without `trust proxy`, Express's req.secure is
// always false, so express-session refuses to set a `secure: true` cookie
// at all (it thinks it would be sending a secure cookie over an insecure
// connection). Net effect: login appears to succeed (redirect fires) but
// no session cookie is ever stored, so the very next request looks
// logged-out and bounces back to /auth/login. `trust proxy: 1` tells
// Express to trust the X-Forwarded-Proto header from exactly one hop
// upstream (the platform's own proxy), which is the correct, safe value
// for single-proxy PaaS deployments like Railway.
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
const PORT = process.env.PORT || 3000;
// Config
const { buildChallengeCards, TOTAL_CHALLENGES } = require('./config/challenges');
const { getEventState } = require('./config/event');
// Auto-generate flag files if missing.
// Bug fix 2: read flag content from DB (single source of truth) instead of hardcoding.
// Falls back to DB values at startup so file contents always match the flags table.
const generateFlagFiles = async () => {
const fileChallenges = [
{ path: path.join(__dirname, 'flag_calc.txt'), challengeId: 6 },
{ path: path.join(__dirname, 'flag_xxe.txt'), challengeId: 9 },
{ path: path.join(__dirname, 'ping_sandbox', 'flag_ping.txt'), challengeId: 4 },
];
for (const { path: fp, challengeId } of fileChallenges) {
if (!fs.existsSync(fp)) {
try {
const row = await db.get('SELECT flag FROM flags WHERE challenge_id = ?', [challengeId]);
if (row) {
const plain = Buffer.from(row.flag, 'base64').toString('utf-8').trim();
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, plain);
console.log(`[flags] Generated ${path.basename(fp)}`);
}
} catch (err) {
console.error(`[flags] Could not generate ${path.basename(fp)}:`, err.message);
}
}
}
};
// Middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' }));
// Bug fix: express's body parsers leave req.body as `undefined` (not `{}`)
// when a request has no matching Content-Type — e.g. a POST with no body,
// or certain CSRF-style cross-origin requests. Any route reading
// req.body.someField in that case throws "Cannot read properties of
// undefined", crashing the request with a 500 and leaking a stack trace.
// This is especially bad here since it's attack-shaped traffic that
// triggers it. Normalise req.body to always be an object.
app.use((req, res, next) => {
if (req.body === undefined) req.body = {};
next();
});
app.use(cookieParser(process.env.SESSION_SECRET || 'secret_key_for_signed_cookies')); // Bug fix 7: use SESSION_SECRET env var
app.use(session({
secret: process.env.SESSION_SECRET || 'aquila_ctf_platform_secret_key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 24 hours
httpOnly: true, // Bug fix 1: JS cannot read the session cookie
secure: process.env.NODE_ENV === 'production', // HTTPS-only in prod
sameSite: 'lax' // CSRF protection at cookie level
}
}));
app.use(express.static('public'));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/phantom-lab', express.static(path.join(__dirname, 'phantom-insider', 'static')));
app.set('view engine', 'ejs');
// Routes
const authRoutes = require('./routes/auth');
const challengeRoutes = require('./routes/challenges');
const apiRoutes = require('./routes/api');
const adminRoutes = require('./routes/admin');
app.use('/auth', authRoutes);
app.use('/challenge', challengeRoutes);
app.use('/admin', adminRoutes);
// Internal endpoint for SSRF challenge (must be at root level, not under /challenge)
app.get('/internal/flag', async (req, res) => {
// Bug fix 1: flag served from DB, not hardcoded
try {
const row = await db.get('SELECT flag FROM flags WHERE challenge_id = ?', [10]);
const plain = row ? Buffer.from(row.flag, 'base64').toString('utf-8').trim() : '';
res.send(plain);
} catch (err) {
console.error('[internal/flag]', err);
res.status(500).send('Error');
}
});
app.use('/', apiRoutes);
app.get('/', requireAuth, async (req, res) => {
try {
const userId = req.session.userId;
// Get solved challenges
const solvedRows = await db.query('SELECT challenge_id FROM user_progress WHERE user_id = ? AND solved_at IS NOT NULL', [userId]);
const solved = solvedRows.map(row => row.challenge_id);
// Initialize timer if not present (using session now)
if (!req.session.startTime) {
req.session.startTime = Date.now();
}
const score = solved.length * 100; // Base score, need to subtract hints
// Calculate actual score with hint penalties
let totalPenalty = 0;
const progressRows = await db.query('SELECT hints_used FROM user_progress WHERE user_id = ?', [userId]);
progressRows.forEach(row => {
totalPenalty += (row.hints_used || 0) * 20;
});
const finalScore = Math.max(0, score - totalPenalty);
const progressPercent = (solved.length / TOTAL_CHALLENGES) * 100;
// Calculate elapsed time
const startTime = req.session.startTime;
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
const hours = Math.floor(elapsedSeconds / 3600);
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
const seconds = elapsedSeconds % 60;
const timeDisplay = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
const event = getEventState();
// Fetch team info if user is on a team
let team = null;
if (req.session.teamId) {
try {
team = await db.get('SELECT name, code FROM teams WHERE id = ?', [req.session.teamId]);
} catch (_) {}
}
res.render('index', {
title: event.name,
score: finalScore,
// Bug fix: previously the view back-derived the hint penalty as
// `solvedCount*100 - score`, which understates it once the real
// penalty exceeds solved points and score is floored at 0.
// Pass the real, unfloored penalty so the breakdown is accurate.
hintPenalty: totalPenalty,
progress: progressPercent,
solved: solved, // array of solved challenge IDs (for buildChallengeCards)
solvedCount: solved.length, // scalar count for display in templates
timeDisplay: timeDisplay,
startTime: req.session.startTime,
user: req.session.username,
isAdmin: req.session.isAdmin || false,
challenges: buildChallengeCards(solved),
totalChallenges: TOTAL_CHALLENGES,
event,
team,
});
} catch (err) {
console.error(err);
res.status(500).render('error', {
statusCode: 500,
title: 'Server Error',
message: 'Something went wrong loading the dashboard. Please try again.',
icon: '⚡'
});
}
});
// 404 Catch-all — must be after all route definitions
app.use((req, res) => {
res.status(404).render('error', {
statusCode: 404,
title: 'Page Not Found',
message: 'The page you are looking for doesn\'t exist or has been moved.',
icon: '🔍'
});
});
// Generate flag files from DB after everything is initialised
generateFlagFiles().catch(err => console.error('[flags] Init error:', err));
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on http://localhost:${PORT}`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
server.close(() => {
console.log('Server closed.');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('\nSIGINT received. Shutting down...');
server.close(() => {
process.exit(0);
});
});