diff --git a/server.js b/server.js index d3cf42f..dc5a4a7 100644 --- a/server.js +++ b/server.js @@ -12,16 +12,52 @@ const port = process.env.PORT || 3000; // Password Hashing Helpers function hashPassword(password) { + if (typeof password !== 'string') throw new Error('Password must be a string'); const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex'); return `${salt}:${hash}`; } function comparePassword(password, storedPassword) { - if (!storedPassword || !storedPassword.includes(':')) return false; - const [salt, hash] = storedPassword.split(':'); - const checkHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex'); - return hash === checkHash; + if (typeof password !== 'string' || !storedPassword || !storedPassword.includes(':')) return false; + try { + const [salt, hash] = storedPassword.split(':'); + const checkHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex'); + return hash === checkHash; + } catch (e) { + return false; + } +} + +// Token Helpers +const secretPath = path.join(__dirname, 'data', 'secret.key'); +let JWT_SECRET; +if (fs.existsSync(secretPath)) { + JWT_SECRET = fs.readFileSync(secretPath, 'utf8'); +} else { + JWT_SECRET = crypto.randomBytes(32).toString('hex'); + if (fs.existsSync(path.join(__dirname, 'data'))) { + fs.writeFileSync(secretPath, JWT_SECRET); + } +} + +function generateToken(teamId) { + const hmac = crypto.createHmac('sha256', JWT_SECRET); + hmac.update(teamId); + return `${teamId}.${hmac.digest('hex')}`; +} + +function verifyToken(token) { + if (!token) return null; + const parts = token.split('.'); + if (parts.length !== 2) return null; + const [teamId, signature] = parts; + const hmac = crypto.createHmac('sha256', JWT_SECRET); + hmac.update(teamId); + if (hmac.digest('hex') === signature) { + return teamId; + } + return null; } // Initialize Database @@ -53,7 +89,7 @@ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadDir), - filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname) + filename: (req, file, cb) => cb(null, Date.now() + '-' + path.basename(file.originalname)) }); const upload = multer({ storage }); @@ -134,8 +170,14 @@ db.serialize(() => { db.run(`INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)`, [k, v]); }); - const adminPass = hashPassword('admin'); - db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', ?, 1, 0)`, [adminPass]); + db.get("SELECT COUNT(*) as count FROM teams WHERE isAdmin = 1 AND id != 'admin-0'", (err, row) => { + if (!err && row && row.count === 0) { + const adminPass = hashPassword('admin'); + db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', ?, 1, 0)`, [adminPass]); + } else if (!err && row && row.count > 0) { + db.run(`DELETE FROM teams WHERE id = 'admin-0'`); + } + }); }); const unlinkFiles = (filesJson) => { @@ -149,6 +191,14 @@ const unlinkFiles = (filesJson) => { } catch (e) { console.error("Error unlinking files:", e); } }; +const SCHEMA_WHITELIST = { + teams: ['id', 'name', 'password', 'isAdmin', 'isDisabled'], + challenges: ['id', 'title', 'category', 'difficulty', 'description', 'initialPoints', 'minimumPoints', 'decaySolves', 'flag', 'files', 'port', 'connectionType', 'overrideIp'], + solves: ['teamId', 'challengeId', 'timestamp'], + blogs: ['id', 'title', 'content', 'timestamp'], + config: ['key', 'value'] +}; + app.use(cors()); app.use(express.json({ limit: '500mb' })); app.use('/files', express.static(uploadDir)); @@ -157,7 +207,8 @@ const apiRouter = express.Router(); apiRouter.use('/admin', async (req, res, next) => { const authHeader = req.headers.authorization; - const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null; + const token = authHeader ? authHeader.replace('Bearer ', '') : null; + const teamId = verifyToken(token); if (!teamId) return res.status(401).json({ message: 'Unauthorized' }); try { @@ -182,7 +233,7 @@ apiRouter.post('/auth/register', (req, res) => { if (err.message.includes('UNIQUE')) return res.status(400).json({ message: 'Team name already exists' }); return res.status(500).json({ message: 'Registration failed' }); } - res.json({ team: { id, name, isAdmin: 0, isDisabled: 0 }, token: `mock-token-${id}` }); + res.json({ team: { id, name, isAdmin: 0, isDisabled: 0 }, token: generateToken(id) }); }); }); @@ -191,8 +242,20 @@ apiRouter.post('/auth/login', (req, res) => { db.get("SELECT * FROM teams WHERE name = ?", [name], (err, team) => { if (err || !team || !comparePassword(password, team.password)) return res.status(401).json({ message: 'Invalid credentials' }); if (team.isDisabled) return res.status(403).json({ message: 'Account disabled' }); + + if (team.id === 'admin-0') { + db.get("SELECT COUNT(*) as count FROM teams WHERE isAdmin = 1 AND id != 'admin-0'", (err, row) => { + if (row && row.count > 0) { + return res.status(403).json({ message: 'Default admin account disabled' }); + } + const { password: _, ...teamData } = team; + res.json({ team: teamData, token: generateToken(team.id) }); + }); + return; + } + const { password: _, ...teamData } = team; - res.json({ team: teamData, token: `mock-token-${team.id}` }); + res.json({ team: teamData, token: generateToken(team.id) }); }); }); @@ -201,7 +264,8 @@ apiRouter.get('/state', async (req, res) => { // Security Check: Identify requester and admin status const authHeader = req.headers.authorization; - const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null; + const token = authHeader ? authHeader.replace('Bearer ', '') : null; + const teamId = verifyToken(token); let isAdmin = false; if (teamId) { @@ -219,7 +283,17 @@ apiRouter.get('/state', async (req, res) => { state.isStarted = state.config.isStarted === 'true'; db.all("SELECT id, name, isAdmin, isDisabled FROM teams", (err, teams) => { if (err) return res.status(500).json({ error: 'Failed to fetch teams' }); - state.teams = teamId ? (teams || []) : []; + + if (isAdmin) { + state.teams = teams || []; + } else { + // For non-admins, only show active non-admin teams for the scoreboard + state.teams = (teams || []).filter(t => !t.isAdmin && !t.isDisabled).map(t => { + const { isAdmin: _, isDisabled: __, ...publicTeam } = t; + return publicTeam; + }); + } + db.all("SELECT * FROM challenges", (err, challenges) => { if (err) return res.status(500).json({ error: 'Failed to fetch challenges' }); db.all("SELECT * FROM solves", (err, solves) => { @@ -314,12 +388,13 @@ apiRouter.post('/admin/db/restore', upload.single('restoreFile'), (req, res) => fs.unlinkSync(req.file.path); db.serialize(() => { db.run("BEGIN TRANSACTION"); - const tables = ['teams', 'challenges', 'solves', 'blogs', 'config']; - tables.forEach(table => { + Object.keys(SCHEMA_WHITELIST).forEach(table => { db.run(`DELETE FROM ${table}`); if (!data[table] || data[table].length === 0) return; const rows = data[table]; - const cols = Object.keys(rows[0]); + const cols = Object.keys(rows[0]).filter(c => SCHEMA_WHITELIST[table].includes(c)); + if (cols.length === 0) return; + const stmt = db.prepare(`INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(()=>'?').join(',')})`); rows.forEach(row => { if (table === 'challenges') { @@ -333,7 +408,8 @@ apiRouter.post('/admin/db/restore', upload.single('restoreFile'), (req, res) => return file; })); } - stmt.run(Object.values(row)); + const params = cols.map(c => row[c]); + stmt.run(params); }); stmt.finalize(); }); @@ -344,7 +420,8 @@ apiRouter.post('/admin/db/restore', upload.single('restoreFile'), (req, res) => apiRouter.post('/challenges/submit', (req, res) => { const authHeader = req.headers.authorization; - const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null; + const token = authHeader ? authHeader.replace('Bearer ', '') : null; + const teamId = verifyToken(token); const { challengeId, flag } = req.body; if (!teamId) return res.status(401).json({ success: false }); db.all("SELECT key, value FROM config", (err, configRows) => { @@ -380,7 +457,9 @@ apiRouter.post('/admin/toggle-ctf', (req, res) => { apiRouter.post('/admin/reset-scores', (req, res) => { db.run("DELETE FROM solves", () => res.json({ success: true })); }); apiRouter.put('/profile', (req, res) => { - const teamId = req.headers.authorization?.replace('Bearer mock-token-', ''); + const authHeader = req.headers.authorization; + const token = authHeader ? authHeader.replace('Bearer ', '') : null; + const teamId = verifyToken(token); if (!teamId) return res.status(401).json({ message: 'Unauthorized' }); db.run("UPDATE teams SET password = ? WHERE id = ?", [hashPassword(req.body.password), teamId], () => res.json({ success: true })); });