From 6b2187de2adb5bde196393317aae9c0c3193e62f Mon Sep 17 00:00:00 2001 From: Brendon Heinst Date: Tue, 24 Feb 2026 17:19:13 +0100 Subject: [PATCH] Security hardening for production deployment - Remove secrets from Dockerfile build args, pass as runtime env vars only - Add non-root user to Docker container - Add SKU format validation to prevent S3 key injection - Sanitize error responses in sanity-lookup route - Fix zod import to use @medusajs/framework/zod - Clean up .env.template defaults and .dockerignore --- .dockerignore | 2 ++ .env.template | 10 +++++----- Dockerfile | 18 +++++++----------- .../[id]/download-grants/validators.ts | 4 ++-- src/api/admin/sanity-lookup/route.ts | 3 ++- src/lib/s3-download.ts | 5 +++++ 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.dockerignore b/.dockerignore index f5d8997..cc6d178 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,5 @@ coverage .cache .vscode .idea +.claude +CLAUDE.md diff --git a/.env.template b/.env.template index 4036a84..0e41016 100644 --- a/.env.template +++ b/.env.template @@ -1,9 +1,9 @@ -STORE_CORS=http://localhost:8000,https://docs.medusajs.com -ADMIN_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com -AUTH_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com +STORE_CORS=http://localhost:3000 +ADMIN_CORS=http://localhost:5173,http://localhost:9000 +AUTH_CORS=http://localhost:5173,http://localhost:9000,http://localhost:3000 REDIS_URL=redis://localhost:6379 -JWT_SECRET=supersecret -COOKIE_SECRET=supersecret +JWT_SECRET= +COOKIE_SECRET= DATABASE_URL= DB_NAME=medusa-v2 diff --git a/Dockerfile b/Dockerfile index 5487fc5..41203a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,15 @@ FROM node:20-alpine AS builder WORKDIR /app +# Only build-time vars needed for admin dashboard compilation ARG STORE_CORS ARG ADMIN_CORS ARG AUTH_CORS -ARG DATABASE_URL -ARG REDIS_URL -ARG JWT_SECRET -ARG COOKIE_SECRET ARG MEDUSA_BACKEND_URL ENV STORE_CORS=$STORE_CORS ENV ADMIN_CORS=$ADMIN_CORS ENV AUTH_CORS=$AUTH_CORS -ENV DATABASE_URL=$DATABASE_URL -ENV REDIS_URL=$REDIS_URL -ENV JWT_SECRET=$JWT_SECRET -ENV COOKIE_SECRET=$COOKIE_SECRET ENV MEDUSA_BACKEND_URL=$MEDUSA_BACKEND_URL COPY package.json package-lock.json ./ @@ -27,13 +20,16 @@ RUN npm run build FROM node:20-alpine WORKDIR /app/server -COPY --from=builder /app/.medusa/server . +RUN addgroup -S medusa && adduser -S medusa -G medusa + +COPY --from=builder --chown=medusa:medusa /app/.medusa/server . RUN npm install --legacy-peer-deps -COPY start.sh . -COPY trptk-pricing.json . +COPY --chown=medusa:medusa start.sh . +COPY --chown=medusa:medusa trptk-pricing.json . RUN chmod +x start.sh +USER medusa ENV NODE_ENV=production EXPOSE 9000 CMD ["sh", "start.sh"] diff --git a/src/api/admin/customers/[id]/download-grants/validators.ts b/src/api/admin/customers/[id]/download-grants/validators.ts index c3eb279..e5bc00f 100644 --- a/src/api/admin/customers/[id]/download-grants/validators.ts +++ b/src/api/admin/customers/[id]/download-grants/validators.ts @@ -1,7 +1,7 @@ -import { z } from "zod" +import { z } from "@medusajs/framework/zod" export const PostAdminDownloadGrant = z.object({ - sku: z.string().min(1), + sku: z.string().regex(/^[A-Za-z]{2,5}\d{3,5}_[A-Za-z0-9]+$/, "Invalid SKU format"), product_title: z.string().min(1), variant_title: z.string().min(1), note: z.string().optional(), diff --git a/src/api/admin/sanity-lookup/route.ts b/src/api/admin/sanity-lookup/route.ts index f44fa78..e6ff4b2 100644 --- a/src/api/admin/sanity-lookup/route.ts +++ b/src/api/admin/sanity-lookup/route.ts @@ -96,8 +96,9 @@ export async function GET( available_variants: release.availableVariants || null, }) } catch (err: any) { + console.error("Sanity query failed:", err) res.status(500).json({ - message: `Sanity query failed: ${err.message}`, + message: "Sanity query failed", }) } } diff --git a/src/lib/s3-download.ts b/src/lib/s3-download.ts index f1e54e1..7e62adb 100644 --- a/src/lib/s3-download.ts +++ b/src/lib/s3-download.ts @@ -25,7 +25,12 @@ function getS3Client(): S3Client { * * Convention: SKU "TTK0001_352K24B2CH" → key "ttk0001/ttk0001_352k24b2ch.zip" */ +const SKU_PATTERN = /^[A-Za-z]{2,5}\d{3,5}_[A-Za-z0-9]+$/ + export function skuToS3Key(sku: string): string { + if (!SKU_PATTERN.test(sku)) { + throw new Error(`Invalid SKU format: ${sku}`) + } const lower = sku.toLowerCase() const catalogueNumber = lower.split("_")[0] return `${catalogueNumber}/${lower}.zip`