diff --git a/.gitignore b/.gitignore index 02d088c..6a8d91d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ .astro/ test-results/ *.log +.env +.env.* diff --git a/docker-compose.yml b/docker-compose.yml index 2db9ae2..bd3b74f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,8 @@ services: restart: unless-stopped ports: - "0.0.0.0:4000:4000" + env_file: .env environment: - HOST=0.0.0.0 - PORT=4000 - - PDS_APP_PASSWORD=5wxb-dk6w-vuth-lgbp - ATAUTH_GATEWAY_URL=http://172.17.0.1:3100 diff --git a/src/lib/api.ts b/src/lib/api.ts index 203cba6..f9cf4bd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -4,7 +4,7 @@ import { BLOG_URL, PDS_URL, DID } from "./constants"; export function checkOrigin(request: Request): Response | null { const origin = request.headers.get("origin"); - if (origin && origin !== BLOG_URL) { + if (!origin || origin !== BLOG_URL) { return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 }); } return null; diff --git a/src/middleware.ts b/src/middleware.ts index a6643f5..8bde7c7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,7 +4,7 @@ export const onRequest = defineMiddleware(async (_context, next) => { const response = await next(); response.headers.set( "Content-Security-Policy", - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://arcnode.xyz https://*.bsky.network; connect-src 'self'; frame-ancestors 'none'" + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://arcnode.xyz; connect-src 'self'; frame-ancestors 'none'" ); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("X-Frame-Options", "DENY"); diff --git a/src/pages/api/update.ts b/src/pages/api/update.ts index 87f48de..603c902 100644 --- a/src/pages/api/update.ts +++ b/src/pages/api/update.ts @@ -21,7 +21,10 @@ export const POST: APIRoute = async ({ request, cookies }) => { const title = typeof body.title === "string" ? body.title.trim() : ""; const content = typeof body.content === "string" ? body.content.trim() : ""; const visibility = typeof body.visibility === "string" ? body.visibility : ""; - const createdAt = typeof body.createdAt === "string" ? body.createdAt : new Date().toISOString(); + const rawCreatedAt = typeof body.createdAt === "string" ? body.createdAt : ""; + const createdAt = rawCreatedAt && !isNaN(Date.parse(rawCreatedAt)) + ? new Date(rawCreatedAt).toISOString() + : new Date().toISOString(); if (!rkey || !isValidRkey(rkey)) { return new Response(JSON.stringify({ error: "Invalid rkey" }), { status: 400 }); diff --git a/src/pages/api/upload-image.ts b/src/pages/api/upload-image.ts index cbb341c..5fb84f3 100644 --- a/src/pages/api/upload-image.ts +++ b/src/pages/api/upload-image.ts @@ -46,11 +46,43 @@ export const POST: APIRoute = async ({ request, cookies }) => { ); } + const bytes = await file.arrayBuffer(); + + // Validate file magic bytes to prevent type spoofing + if (bytes.byteLength < 12) { + return new Response( + JSON.stringify({ error: "File too small to be a valid image" }), + { status: 400 } + ); + } + + const header = new Uint8Array(bytes, 0, 12); + const MAGIC: Record = { + "image/png": [0x89, 0x50, 0x4e, 0x47], + "image/jpeg": [0xff, 0xd8, 0xff], + "image/webp": [0x52, 0x49, 0x46, 0x46], + }; + + const expected = MAGIC[file.type]; + if (!expected || !expected.every((b, i) => header[i] === b)) { + return new Response( + JSON.stringify({ error: "File content does not match declared type" }), + { status: 400 } + ); + } + if (file.type === "image/webp") { + const webp = [0x57, 0x45, 0x42, 0x50]; // "WEBP" at offset 8 + if (!webp.every((b, i) => header[8 + i] === b)) { + return new Response( + JSON.stringify({ error: "File content does not match declared type" }), + { status: 400 } + ); + } + } + const [accessJwt, sessionErr] = await createPdsSession(); if (sessionErr) return sessionErr; - const bytes = await file.arrayBuffer(); - const uploadRes = await fetch( `${PDS_URL}/xrpc/com.atproto.repo.uploadBlob`, {