From e6c1615142aa8c4d267582b5827989cbb24e9e82 Mon Sep 17 00:00:00 2001 From: Pyronewbic Date: Sun, 10 May 2026 22:59:53 +0530 Subject: [PATCH] security: helmet, error sanitization, request IDs, trust proxy, npm audit - Helmet security headers (X-Frame-Options, HSTS, etc.) - Error responses sanitized: no internal paths, credentials, or Firestore details - Request ID on every response (X-Request-Id) for error tracing - trust proxy for correct client IP behind Cloud Run LB - npm audit: 0 vulnerabilities - GitHub Actions bumped to v5/v3 (Node 24 compatible) --- .github/workflows/deploy.yml | 6 +-- .github/workflows/test.yml | 4 +- api.js | 72 +++++++++++++++++++++++------------- package-lock.json | 25 +++++++++---- package.json | 1 + 5 files changed, 70 insertions(+), 38 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a810bcc..62bf66f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,14 +17,14 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@v3 with: workload_identity_provider: projects/129850122606/locations/global/workloadIdentityPools/github-pool/providers/github-provider service_account: casecomp-deploy@casecomp-495718.iam.gserviceaccount.com - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@v3 - name: Build and push run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da8b344..b1b1009 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,8 +9,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 with: node-version: 24 - run: npm install diff --git a/api.js b/api.js index ff31afe..e63c9ca 100644 --- a/api.js +++ b/api.js @@ -1,5 +1,7 @@ import "dotenv/config"; +import crypto from "crypto"; import express from "express"; +import helmet from "helmet"; import rateLimit from "express-rate-limit"; import swaggerUi from "swagger-ui-express"; import { swaggerSpec } from "./lib/swagger.js"; @@ -21,8 +23,19 @@ import path from "path"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); + +app.set("trust proxy", true); + +app.use(helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, +})); + app.use(express.json({ limit: "100kb" })); + app.use((req, res, next) => { + req.requestId = crypto.randomUUID().slice(0, 8); + res.setHeader("X-Request-Id", req.requestId); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); @@ -60,9 +73,18 @@ app.use("/api", (req, res, next) => { }); app.use("/v1", apiLimiter); -async function logError(type, message, detail = "") { - console.error(`[ERROR] ${type}: ${message}`); - try { await saveErrorLog({ type, message, detail, ts: new Date().toISOString() }); } catch {} +function safeErrorMessage(e) { + const msg = e.message || String(e); + if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND/.test(msg)) return "Upstream service unavailable"; + if (/api[_-]?key|token|secret|credential/i.test(msg)) return "Authentication error"; + if (/firestore|grpc|google/i.test(msg)) return "Internal storage error"; + if (msg.length > 200) return msg.slice(0, 200); + return msg; +} + +async function logError(type, message, detail = "", requestId = "") { + console.error(`[ERROR] [${requestId}] ${type}: ${message}`); + try { await saveErrorLog({ type, message, detail, requestId, ts: new Date().toISOString() }); } catch {} } const clientId = process.env.EBAY_CLIENT_ID; @@ -210,8 +232,8 @@ app.get("/api/search", apiAuthMiddleware, (req, res, next) => { req._errorType = res.json(result); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -252,8 +274,8 @@ app.get("/api/sold", apiAuthMiddleware, (req, res, next) => { req._errorType = " res.json({ query: q, sold, soldSource, counts: { sold: sold.length } }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -265,8 +287,8 @@ app.get("/api/psa", authMiddleware, (req, res, next) => { req._errorType = "psa" const signal = await getPsaGradingSignal(q); res.json({ query: q, signal }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -304,8 +326,8 @@ app.post("/api/grade", authMiddleware, (req, res, next) => { req._errorType = "g res.json({ grade, stored: !!(grade && !grade.error) }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -316,8 +338,8 @@ app.get("/api/grades", authMiddleware, async (req, res) => { const records = await getGradeLogs({ limit, query: req.query.q, source: req.query.source }); res.json(records); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -393,8 +415,8 @@ v1.get("/drops", async (req, res) => { const records = await getDrops({ limit, site: req.query.site, status: req.query.status }); res.json({ drops: records, count: records.length, limit }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -405,8 +427,8 @@ v1.get("/drops/:id", async (req, res) => { if (!record) return res.status(404).json({ error: "Drop not found" }); res.json(record); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -448,8 +470,8 @@ v1.get("/comps", async (req, res) => { sold: { items: sold, count: sold.length }, }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -478,8 +500,8 @@ v1.get("/webhooks", async (req, res) => { const stored = await getWebhooks(); res.json({ webhooks: stored, count: stored.length }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -512,8 +534,8 @@ app.post("/api/drop-event", authMiddleware, (req, res, next) => { req._errorType } res.json(drop); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); @@ -525,8 +547,8 @@ app.post("/api/alerts", authMiddleware, async (req, res) => { await saveAlert({ email, targetPrice: targetPrice || null, query, createdAt: new Date().toISOString() }); res.json({ ok: true }); } catch (e) { - logError(req._errorType || "api", e.message, req.originalUrl); - res.status(500).json({ error: e.message }); + logError(req._errorType || "api", e.message, req.originalUrl, req.requestId); + res.status(500).json({ error: safeErrorMessage(e), requestId: req.requestId }); } }); diff --git a/package-lock.json b/package-lock.json index 3bfb23c..2cda791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dotenv": "^16.4.7", "express": "^5.2.1", "express-rate-limit": "^8.3.2", + "helmet": "^8.1.0", "ioredis": "^5.10.1", "minimist": "^1.2.8", "playwright": "^1.59.1", @@ -22,7 +23,7 @@ "tough-cookie": "^5.1.2" }, "engines": { - "node": ">=20" + "node": ">=24" } }, "node_modules/@google-cloud/firestore": { @@ -969,11 +970,11 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -1343,6 +1344,14 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -1455,9 +1464,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "engines": { "node": ">= 12" } diff --git a/package.json b/package.json index 1fd5eed..90ccd18 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dotenv": "^16.4.7", "express": "^5.2.1", "express-rate-limit": "^8.3.2", + "helmet": "^8.1.0", "ioredis": "^5.10.1", "minimist": "^1.2.8", "playwright": "^1.59.1",