diff --git a/frontend/leaderboard.html b/frontend/leaderboard.html index 242c6f83..6d426e62 100644 --- a/frontend/leaderboard.html +++ b/frontend/leaderboard.html @@ -124,7 +124,7 @@

Leaderboard

- diff --git a/package-lock.json b/package-lock.json index cb51fe5f..3d201b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "axios": "^1.10.0", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "helmet": "^8.2.0" }, "devDependencies": { "prettier": "^3.8.3" @@ -562,6 +563,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", diff --git a/package.json b/package.json index 25079eb8..d22495e7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dependencies": { "axios": "^1.10.0", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "helmet": "^8.2.0" }, "devDependencies": { "prettier": "^3.8.3" diff --git a/server.js b/server.js index 298ff019..3fa8ddd2 100644 --- a/server.js +++ b/server.js @@ -1,32 +1,104 @@ const express = require("express"); +const helmet = require("helmet"); const cors = require("cors"); const path = require("path"); +const fs = require("fs"); +const crypto = require("crypto"); +const fetchStudentHistory = require("./scripts/fetch-student-info"); const app = express(); const PORT = process.env.PORT || 3000; -const fetchStudentHistory = require("./scripts/fetch-student-info"); - app.use(cors()); -app.use(express.static(path.join(__dirname, "frontend"))); -/* ---------------- HOME ROUTES ---------------- */ +// 1. Per-request nonce generator (used by CSP and HTML nonce injection) +app.use((req, res, next) => { + res.locals.nonce = crypto.randomBytes(16).toString("base64url"); + next(); +}); + +// 2. Security headers via Helmet +// Sets: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, +// Referrer-Policy, Strict-Transport-Security, and more. +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + // Allow fetch/XHR to external APIs used by the frontend + connectSrc: [ + "'self'", + "https://raw.githubusercontent.com", + "https://leetcode-api-dun.vercel.app", + "https://lc-backend-lyq2.onrender.com", + ], + // Inline scripts need a per-request nonce; external scripts from 'self' + // are allowed automatically. + scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], + // Allow inline styles (style attributes) + Google Fonts stylesheet + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], + // Google Fonts + fontSrc: ["'self'", "https://fonts.gstatic.com"], + // Images: self + data: URIs (used by matrix canvas) + imgSrc: ["'self'", "data:"], + // No plugins + objectSrc: ["'none'"], + // No framing (clickjacking protection) + frameAncestors: ["'none'"], + // Upgrade HTTP to HTTPS when possible + upgradeInsecureRequests: [], + }, + }, + crossOriginEmbedderPolicy: false, // keep false so Google Fonts load + }), +); + +// 3. Static assets (JS, CSS, images) — served normally +// HTML files are excluded — they go through nonce injection via routes. +const staticMiddleware = express.static(path.join(__dirname, "frontend"), { + index: false, +}); + +app.use((req, res, next) => { + if (req.path.endsWith(".html")) return next(); + staticMiddleware(req, res, next); +}); + +// 4. HTML page routes — inject per-request nonce into __NONCE__ placeholders +function serveHtml(res, filePath) { + fs.readFile(filePath, "utf8", (err, data) => { + if (err) { + return res.status(500).send("Error loading page"); + } + const html = data.replace(/__NONCE__/g, res.locals.nonce); + res.type("html").send(html); + }); +} + +/* HOME ROUTES */ app.get("/", (req, res) => { - res.sendFile(path.join(__dirname, "frontend", "index.html")); + serveHtml(res, path.join(__dirname, "frontend", "index.html")); }); app.get("/leaderboard", (req, res) => { - res.sendFile(path.join(__dirname, "frontend", "leaderboard.html")); + serveHtml(res, path.join(__dirname, "frontend", "leaderboard.html")); }); app.get("/about", (req, res) => { - res.sendFile(path.join(__dirname, "frontend", "about.html")); + serveHtml(res, path.join(__dirname, "frontend", "about.html")); }); app.get("/registration", (req, res) => { - res.sendFile(path.join(__dirname, "frontend", "registration.html")); + serveHtml(res, path.join(__dirname, "frontend", "registration.html")); +}); + +// Redirect direct .html file access so nonce injection still applies +app.get(/\.html$/, (req, res) => { + const cleanPath = req.path.replace(/\.html$/, ""); + res.redirect(301, cleanPath || "/"); }); +// 5. Utility endpoints app.get("/uptime", (req, res) => { res.json({ status: "Website is running ✅" }); });