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 ✅" });
});