- 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:
m0rph3us1987
2026-03-10 13:29:50 +01:00
parent b8cc7dda8b
commit 27566a7813
12 changed files with 121 additions and 79 deletions

123
server.js
View File

@@ -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) => {