assertVerifiedForEmail($email, $lastUserActivityAt); if (($auth['ok'] ?? false) !== true) { $reason = (string) ($auth['reason'] ?? ''); Bootstrap::jsonResponse([ 'ok' => false, 'message' => (string) ($auth['message'] ?? 'Bitte E-Mail erneut verifizieren.'), 'auth_required' => $reason === 'auth_required', 'auth_expired' => $reason === 'auth_expired', ], (int) ($auth['status_code'] ?? 401)); } $field = trim((string) ($_GET['field'] ?? '')); $index = (int) ($_GET['index'] ?? -1); if ($field === '' || $index < 0) { Bootstrap::textResponse(Bootstrap::appMessage('upload_preview.invalid_upload_entry'), 422); } /** @return string|null */ function resolveStoredPreviewPath(array $entry, array $app): ?string { $baseDir = rtrim((string) ($app['storage']['uploads'] ?? ''), '/'); if ($baseDir === '') { return null; } $storedDir = trim((string) ($entry['stored_dir'] ?? ''), '/'); $storedFilename = trim((string) ($entry['stored_filename'] ?? '')); if ($storedDir === '' || $storedFilename === '') { return null; } if (str_contains($storedDir, '..') || str_contains($storedFilename, '..')) { return null; } if (!preg_match('/^[A-Za-z0-9._\/-]+$/', $storedDir)) { return null; } if (!preg_match('/^[A-Za-z0-9._ -]+$/', $storedFilename)) { return null; } $path = $baseDir . '/' . $storedDir . '/' . $storedFilename; $realBase = realpath($baseDir); $realPath = realpath($path); if ($realBase !== false && $realPath !== false && !str_starts_with($realPath, $realBase)) { return null; } return $path; } $app = Bootstrap::config('app'); $limiter = new RateLimiter(); $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $rateKey = sprintf('preview-upload:%s:%s', $ip, $email); if (!$limiter->allow($rateKey, (int) $app['rate_limit']['requests'], (int) $app['rate_limit']['window_seconds'])) { Bootstrap::textResponse(Bootstrap::appMessage('upload_preview.rate_limited'), 429); } $store = new JsonStore(); $draft = $store->getDraft($email); if (!is_array($draft)) { Bootstrap::textResponse(Bootstrap::appMessage('upload_preview.draft_not_found'), 404); } $uploads = (array) ($draft['uploads'] ?? []); $files = $uploads[$field] ?? null; $entry = (is_array($files) && isset($files[$index]) && is_array($files[$index])) ? $files[$index] : null; if (!is_array($entry)) { Bootstrap::textResponse(Bootstrap::appMessage('upload_preview.upload_not_found'), 404); } $path = resolveStoredPreviewPath($entry, $app); if ($path === null || !is_file($path)) { Bootstrap::textResponse(Bootstrap::appMessage('upload_preview.file_not_found'), 404); } $mime = (string) ($entry['mime'] ?? ''); if ($mime === '') { $detected = @mime_content_type($path); $mime = is_string($detected) ? $detected : 'application/octet-stream'; } $downloadName = (string) ($entry['original_filename'] ?? basename($path)); $fallbackName = preg_replace('/[^A-Za-z0-9._-]/', '_', $downloadName) ?: 'upload.bin'; $encodedName = rawurlencode($downloadName); header('Content-Type: ' . $mime); header('X-Content-Type-Options: nosniff'); header('Content-Length: ' . (string) filesize($path)); header('Content-Disposition: inline; filename="' . $fallbackName . '"; filename*=UTF-8\'\'' . $encodedName); readfile($path); exit;