- Added "HW" (Hardware) category to the platform with a dedicated icon and color
- Updated challenge grid to 6 columns on desktop to accommodate the new category - Alphabetized challenge categories in the main view and Admin panel selection - Alphabetized operators list in the Admin panel with case-insensitive sorting - Restricted visibility of Challenges, Scoreboard, and Score Matrix to authenticated users only - Secured the /state API endpoint to prevent leaking challenges, solves, teams, or internal IP (dockerIp) to guests - Implemented server-side verification of user profile in the state response to prevent client-side admin spoofing - Refactored the /state backend endpoint using async/await for better reliability and error handling - Rebranded the project from "cypherstrike-ctf" to "hipctf" across package.json, index.html, and server defaults - Synchronized browser page title with the competition name configured in the Admin panel - Fixed a "black page" issue by resolving a missing React import and adding frontend sanity checks
This commit is contained in:
123
server.js
123
server.js
@@ -152,7 +152,7 @@ db.serialize(() => {
|
||||
|
||||
const defaults = [
|
||||
['isStarted', 'false'],
|
||||
['conferenceName', 'HIP'],
|
||||
['conferenceName', 'HIPCTF'],
|
||||
['landingText', 'WELCOME TO THE PLAYGROUND. SOLVE CHALLENGES. SHARE KNOWLEDGE. 🦄'],
|
||||
['logoData', ''],
|
||||
['bgType', 'color'],
|
||||
@@ -260,72 +260,73 @@ apiRouter.post('/auth/login', (req, res) => {
|
||||
});
|
||||
|
||||
apiRouter.get('/state', async (req, res) => {
|
||||
const state = { isStarted: false, teams: [], challenges: [], solves: [], blogs: [], config: {} };
|
||||
|
||||
// Security Check: Identify requester and admin status
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||
const teamId = verifyToken(token);
|
||||
let isAdmin = false;
|
||||
try {
|
||||
const state = { isStarted: false, teams: [], challenges: [], solves: [], blogs: [], config: {}, user: null };
|
||||
|
||||
// Security Check: Identify requester and admin status
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||
const teamId = verifyToken(token);
|
||||
let isAdmin = false;
|
||||
|
||||
if (teamId) {
|
||||
try {
|
||||
const team = await dbGet("SELECT isAdmin FROM teams WHERE id = ?", [teamId]);
|
||||
isAdmin = team && team.isAdmin === 1;
|
||||
} catch (err) {
|
||||
console.error("Auth verify failed in state API:", err);
|
||||
if (teamId) {
|
||||
const team = await dbGet("SELECT id, name, isAdmin, isDisabled FROM teams WHERE id = ?", [teamId]);
|
||||
if (team) {
|
||||
state.user = { id: team.id, name: team.name, isAdmin: team.isAdmin === 1 };
|
||||
isAdmin = team.isAdmin === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.all("SELECT key, value FROM config", (err, configRows) => {
|
||||
if (err) return res.status(500).json({ error: 'Failed to fetch config' });
|
||||
const configRows = await dbAll("SELECT key, value FROM config");
|
||||
configRows.forEach(row => { state.config[row.key] = row.value; });
|
||||
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' });
|
||||
|
||||
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) => {
|
||||
if (err) return res.status(500).json({ error: 'Failed to fetch solves' });
|
||||
db.all("SELECT * FROM blogs ORDER BY timestamp DESC", (err, blogs) => {
|
||||
if (err) return res.status(500).json({ error: 'Failed to fetch blogs' });
|
||||
state.solves = solves || [];
|
||||
state.blogs = blogs || [];
|
||||
const startTime = parseInt(state.config.eventStartTime || "0", 10);
|
||||
const isBeforeStart = Date.now() < startTime;
|
||||
if (!teamId || (!isAdmin && (!state.isStarted || isBeforeStart))) {
|
||||
state.challenges = [];
|
||||
} else {
|
||||
state.challenges = (challenges || []).map(c => {
|
||||
const enriched = {
|
||||
...c,
|
||||
files: JSON.parse(c.files || '[]'),
|
||||
solves: state.solves.filter(s => s.challengeId === c.id).map(s => s.teamId)
|
||||
};
|
||||
// CRITICAL SECURITY FIX: Hide flag if not admin
|
||||
if (!isAdmin) {
|
||||
delete enriched.flag;
|
||||
}
|
||||
return enriched;
|
||||
});
|
||||
}
|
||||
res.json(state);
|
||||
});
|
||||
});
|
||||
if (!teamId) delete state.config.dockerIp;
|
||||
|
||||
const startTime = parseInt(state.config.eventStartTime || "0", 10);
|
||||
const isBeforeStart = Date.now() < startTime;
|
||||
|
||||
// Fetch teams
|
||||
const teams = await dbAll("SELECT id, name, isAdmin, isDisabled FROM teams");
|
||||
if (!teamId || (!isAdmin && (!state.isStarted || isBeforeStart))) {
|
||||
state.teams = [];
|
||||
} else if (isAdmin) {
|
||||
state.teams = teams || [];
|
||||
} else {
|
||||
state.teams = (teams || []).filter(t => !t.isAdmin && !t.isDisabled).map(t => {
|
||||
const { isAdmin: _, isDisabled: __, ...publicTeam } = t;
|
||||
return publicTeam;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch solves
|
||||
const solves = await dbAll("SELECT * FROM solves");
|
||||
|
||||
// Fetch challenges
|
||||
const challenges = await dbAll("SELECT * FROM challenges");
|
||||
if (!teamId || (!isAdmin && (!state.isStarted || isBeforeStart))) {
|
||||
state.challenges = [];
|
||||
state.solves = [];
|
||||
} else {
|
||||
state.solves = solves || [];
|
||||
state.challenges = (challenges || []).map(c => {
|
||||
const enriched = {
|
||||
...c,
|
||||
files: JSON.parse(c.files || '[]'),
|
||||
solves: (solves || []).filter(s => s.challengeId === c.id).map(s => s.teamId)
|
||||
};
|
||||
if (!isAdmin) delete enriched.flag;
|
||||
return enriched;
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch blogs
|
||||
state.blogs = await dbAll("SELECT * FROM blogs ORDER BY timestamp DESC");
|
||||
|
||||
res.json(state);
|
||||
} catch (err) {
|
||||
console.error("State API Error:", err);
|
||||
res.status(500).json({ error: 'Failed to fetch state' });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.put('/admin/config', upload.fields([{ name: 'logo' }, { name: 'bgImage' }]), (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user