Enhance CORS and CSP configurations in Express app

This commit is contained in:
Julio Cesar
2025-08-20 17:26:28 +02:00
parent cede717994
commit 225c1b4cb8
4 changed files with 402 additions and 11 deletions

View File

@@ -5,17 +5,65 @@ import fs from "fs"
import { fileURLToPath } from "url"
import { dirname } from "path"
import { MetersCollection, MeterFeature } from "shared-types/meters"
import { randomBytes } from "crypto"
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const app: Express = express()
// Config
const PORT = process.env.PORT || 3001
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173"
// Middleware
app.use(cors())
app.use(express.json())
// Create app factory so tests can import without starting the server
function createApp(): Express {
const app: Express = express()
// Restrict CORS to the predefined frontend URL
app.use(
cors({
origin: FRONTEND_URL,
})
)
app.use(express.json())
// Require Origin header and reject mismatched origins
app.use((req, res, next) => {
const origin = req.headers.origin as string | undefined
if (!origin) {
return res.status(403).json({ error: "Missing Origin header" })
}
if (origin !== FRONTEND_URL) {
return res.status(403).json({ error: "Forbidden origin" })
}
next()
})
// Per-request CSP nonce and header. Also expose nonce via X-CSP-Nonce so the frontend
// can apply it to inline scripts/styles when needed.
app.use((req, res, next) => {
const nonce = randomBytes(16).toString("base64")
const csp = [
`default-src 'self' ${FRONTEND_URL}`,
`connect-src 'self' ${FRONTEND_URL}`,
`img-src 'self' data: ${FRONTEND_URL}`,
`script-src 'self' 'nonce-${nonce}' ${FRONTEND_URL}`,
`style-src 'self' 'nonce-${nonce}' ${FRONTEND_URL}`,
].join("; ")
res.setHeader("Content-Security-Policy", csp)
// Expose nonce so frontend templates can use it for inline scripts/styles
res.setHeader("X-CSP-Nonce", nonce)
// Vary on Origin so caches consider the Origin header when caching responses
res.setHeader("Vary", "Origin")
next()
})
return app
}
const app = createApp()
// Route to serve the saudi_meters.json file
app.get("/api/meters", (req, res) => {
@@ -71,10 +119,14 @@ app.get("/", (req, res) => {
})
})
// Start the server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`)
console.log(`📊 Meters data available at http://localhost:${PORT}/api/meters`)
})
// Start the server only when run directly (not when imported by tests)
if (process.argv[1] === fileURLToPath(import.meta.url)) {
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`)
console.log(
`📊 Meters data available at http://localhost:${PORT}/api/meters`
)
})
}
export default app