import fs from 'fs/promises' import path from 'path' import sharp from 'sharp' import { getUserFromToken, verifyToken } from '../../utils/auth.js' // Handle both dev and production paths // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input const getDataPath = (filename) => { const cwd = process.cwd() if (cwd.endsWith('.output')) { // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal return path.join(cwd, '../server/data', filename) } // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal return path.join(cwd, 'server/data', filename) } const GALERIE_DIR = getDataPath('galerie') const GALERIE_METADATA = getDataPath('galerie-metadata.json') async function readGalerieMetadata() { try { const data = await fs.readFile(GALERIE_METADATA, 'utf-8') return JSON.parse(data) } catch (error) { if (error.code === 'ENOENT') { return [] } throw error } } export default defineEventHandler(async (event) => { try { const imageId = getRouterParam(event, 'id') const query = getQuery(event) const isPreview = query.preview === 'true' if (!imageId) { throw createError({ statusCode: 400, statusMessage: 'Bild-ID erforderlich' }) } // Prüfe ob Benutzer eingeloggt ist let isLoggedIn = false const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') if (token) { const decoded = verifyToken(token) if (decoded) { const user = await getUserFromToken(token) if (user && user.active) { isLoggedIn = true } } } const metadata = await readGalerieMetadata() const image = metadata.find(img => img.id === imageId) if (!image) { throw createError({ statusCode: 404, statusMessage: 'Bild nicht gefunden' }) } // Prüfe Zugriffsberechtigung if (!image.isPublic && !isLoggedIn) { throw createError({ statusCode: 403, statusMessage: 'Keine Berechtigung zum Anzeigen dieses Bildes' }) } // Bestimme Dateipfad const filename = isPreview ? image.previewFilename : image.filename // Validiere Dateiname gegen Path-Traversal if (!filename || typeof filename !== 'string') { throw createError({ statusCode: 400, statusMessage: 'Ungültiger Dateiname' }) } // Sanitize filename const sanitizedFilename = path.basename(path.normalize(filename)) if (sanitizedFilename.includes('..') || sanitizedFilename.startsWith('/') || sanitizedFilename.startsWith('\\')) { throw createError({ statusCode: 400, statusMessage: 'Ungültiger Dateiname' }) } const subdir = isPreview ? 'previews' : 'originals' const filePath = path.join(GALERIE_DIR, subdir, sanitizedFilename) // Prüfe ob Datei existiert try { await fs.access(filePath) } catch { throw createError({ statusCode: 404, statusMessage: 'Bilddatei nicht gefunden' }) } // Bestimme MIME-Type const ext = path.extname(filename).toLowerCase() const mimeTypes = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp' } const contentType = mimeTypes[ext] || 'application/octet-stream' // Datei lesen und EXIF-Orientierung korrigieren const imageBuffer = await fs.readFile(filePath) const correctedBuffer = await sharp(imageBuffer) .rotate() // Korrigiert automatisch EXIF-Orientierung .toBuffer() setHeader(event, 'Content-Type', contentType) setHeader(event, 'Cache-Control', 'public, max-age=31536000') return correctedBuffer } catch (error) { console.error('Fehler beim Laden des Bildes:', error) if (error.statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: 'Fehler beim Laden des Bildes' }) } })