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
This commit is contained in:
Brendon Heinst 2026-02-24 17:19:13 +01:00
parent 1c91d57899
commit 6b2187de2a
6 changed files with 23 additions and 19 deletions

View file

@ -9,3 +9,5 @@ coverage
.cache .cache
.vscode .vscode
.idea .idea
.claude
CLAUDE.md

View file

@ -1,9 +1,9 @@
STORE_CORS=http://localhost:8000,https://docs.medusajs.com STORE_CORS=http://localhost:3000
ADMIN_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com ADMIN_CORS=http://localhost:5173,http://localhost:9000
AUTH_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com AUTH_CORS=http://localhost:5173,http://localhost:9000,http://localhost:3000
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
JWT_SECRET=supersecret JWT_SECRET=
COOKIE_SECRET=supersecret COOKIE_SECRET=
DATABASE_URL= DATABASE_URL=
DB_NAME=medusa-v2 DB_NAME=medusa-v2

View file

@ -1,22 +1,15 @@
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Only build-time vars needed for admin dashboard compilation
ARG STORE_CORS ARG STORE_CORS
ARG ADMIN_CORS ARG ADMIN_CORS
ARG AUTH_CORS ARG AUTH_CORS
ARG DATABASE_URL
ARG REDIS_URL
ARG JWT_SECRET
ARG COOKIE_SECRET
ARG MEDUSA_BACKEND_URL ARG MEDUSA_BACKEND_URL
ENV STORE_CORS=$STORE_CORS ENV STORE_CORS=$STORE_CORS
ENV ADMIN_CORS=$ADMIN_CORS ENV ADMIN_CORS=$ADMIN_CORS
ENV AUTH_CORS=$AUTH_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 ENV MEDUSA_BACKEND_URL=$MEDUSA_BACKEND_URL
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
@ -27,13 +20,16 @@ RUN npm run build
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app/server 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 RUN npm install --legacy-peer-deps
COPY start.sh . COPY --chown=medusa:medusa start.sh .
COPY trptk-pricing.json . COPY --chown=medusa:medusa trptk-pricing.json .
RUN chmod +x start.sh RUN chmod +x start.sh
USER medusa
ENV NODE_ENV=production ENV NODE_ENV=production
EXPOSE 9000 EXPOSE 9000
CMD ["sh", "start.sh"] CMD ["sh", "start.sh"]

View file

@ -1,7 +1,7 @@
import { z } from "zod" import { z } from "@medusajs/framework/zod"
export const PostAdminDownloadGrant = z.object({ 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), product_title: z.string().min(1),
variant_title: z.string().min(1), variant_title: z.string().min(1),
note: z.string().optional(), note: z.string().optional(),

View file

@ -96,8 +96,9 @@ export async function GET(
available_variants: release.availableVariants || null, available_variants: release.availableVariants || null,
}) })
} catch (err: any) { } catch (err: any) {
console.error("Sanity query failed:", err)
res.status(500).json({ res.status(500).json({
message: `Sanity query failed: ${err.message}`, message: "Sanity query failed",
}) })
} }
} }

View file

@ -25,7 +25,12 @@ function getS3Client(): S3Client {
* *
* Convention: SKU "TTK0001_352K24B2CH" key "ttk0001/ttk0001_352k24b2ch.zip" * 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 { export function skuToS3Key(sku: string): string {
if (!SKU_PATTERN.test(sku)) {
throw new Error(`Invalid SKU format: ${sku}`)
}
const lower = sku.toLowerCase() const lower = sku.toLowerCase()
const catalogueNumber = lower.split("_")[0] const catalogueNumber = lower.split("_")[0]
return `${catalogueNumber}/${lower}.zip` return `${catalogueNumber}/${lower}.zip`