Encrypt file paths in API URLs using Fernet tokens
Raw filesystem paths were exposed in browser URLs, dev tools, and proxy logs. Now all file-serving endpoints accept an opaque encrypted token (t= param) derived from the session secret via HKDF, with a 4-hour TTL. Backend: - Add core/path_tokens.py with Fernet encrypt/decrypt (HKDF from .session_secret) - Add file_token to all list/gallery/feed/search responses across 7 routers - Accept optional t= param on all file-serving endpoints (backward compatible) Frontend: - Update 4 URL helpers in api.ts to prefer token when available - Add 4 new helpers for paid-content/embedded-metadata URLs - Update all 14 page/component files to pass file_token to URL builders - Add file_token to all relevant TypeScript interfaces Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,7 @@ export interface Download {
|
||||
content_type: string | null
|
||||
filename: string | null
|
||||
file_path: string | null
|
||||
file_token?: string
|
||||
file_size: number | null
|
||||
download_date: string
|
||||
status: string
|
||||
@@ -356,6 +357,7 @@ export interface MediaGalleryItem {
|
||||
height?: number
|
||||
duration?: number | null
|
||||
video_id?: string | null
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -603,6 +605,7 @@ export interface PaidContentAttachment {
|
||||
download_attempts: number
|
||||
downloaded_at: string | null
|
||||
created_at: string | null
|
||||
file_token?: string
|
||||
}
|
||||
|
||||
export interface PaidContentEmbed {
|
||||
@@ -809,6 +812,7 @@ export interface ReviewFile {
|
||||
height?: number
|
||||
video_id?: string | null
|
||||
original_path?: string
|
||||
file_token?: string
|
||||
face_recognition?: {
|
||||
scanned: boolean
|
||||
matched?: boolean
|
||||
@@ -1833,15 +1837,17 @@ class APIClient {
|
||||
}>(`/media/gallery/date-range${qs ? '?' + qs : ''}`).then(r => r.ranges)
|
||||
}
|
||||
|
||||
getMediaPreviewUrl(filePath: string) {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
// Tokens in URLs are logged in browser history and server logs
|
||||
getMediaPreviewUrl(filePath: string, fileToken?: string) {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/preview?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/media/preview?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getMediaThumbnailUrl(filePath: string, mediaType: 'image' | 'video') {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
// Tokens in URLs are logged in browser history and server logs
|
||||
getMediaThumbnailUrl(filePath: string, mediaType: 'image' | 'video', fileToken?: string) {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/thumbnail?t=${encodeURIComponent(fileToken)}&media_type=${mediaType}`
|
||||
}
|
||||
return `${API_BASE}/media/thumbnail?file_path=${encodeURIComponent(filePath)}&media_type=${mediaType}`
|
||||
}
|
||||
|
||||
@@ -2216,19 +2222,51 @@ class APIClient {
|
||||
}>(`/monitoring/history?${params.toString()}`)
|
||||
}
|
||||
|
||||
getReviewThumbnailUrl(filePath: string): string {
|
||||
getReviewThumbnailUrl(filePath: string, fileToken?: string): string {
|
||||
// Determine media type from file extension
|
||||
const isVideo = filePath.match(/\.(mp4|mov|webm|avi|mkv|flv|m4v)$/i)
|
||||
const mediaType = isVideo ? 'video' : 'image'
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/thumbnail?t=${encodeURIComponent(fileToken)}&media_type=${mediaType}`
|
||||
}
|
||||
return `${API_BASE}/media/thumbnail?file_path=${encodeURIComponent(filePath)}&media_type=${mediaType}`
|
||||
}
|
||||
|
||||
getReviewPreviewUrl(filePath: string): string {
|
||||
// Security: Auth via httpOnly cookie only - no token in URL
|
||||
getReviewPreviewUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/review/file?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/review/file?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getMediaEmbeddedMetadataUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/media/embedded-metadata?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/media/embedded-metadata?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getPaidContentServeUrl(localPath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/files/serve?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/files/serve?path=${encodeURIComponent(localPath)}`
|
||||
}
|
||||
|
||||
getPaidContentThumbnailUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/thumbnail?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/thumbnail?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
getPaidContentPreviewUrl(filePath: string, fileToken?: string): string {
|
||||
if (fileToken) {
|
||||
return `${API_BASE}/paid-content/preview?t=${encodeURIComponent(fileToken)}`
|
||||
}
|
||||
return `${API_BASE}/paid-content/preview?file_path=${encodeURIComponent(filePath)}`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Smart Folders Methods
|
||||
// ============================================================================
|
||||
@@ -2239,6 +2277,7 @@ class APIClient {
|
||||
count: number
|
||||
previews: Array<{
|
||||
file_path: string
|
||||
file_token?: string
|
||||
content_type: string
|
||||
}>
|
||||
}>
|
||||
@@ -2254,6 +2293,7 @@ class APIClient {
|
||||
recent_downloads: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2265,6 +2305,7 @@ class APIClient {
|
||||
recent_deleted: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2277,6 +2318,7 @@ class APIClient {
|
||||
recent_moved_to_review: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
@@ -2473,6 +2515,7 @@ class APIClient {
|
||||
results: Array<{
|
||||
id: number
|
||||
file_path: string
|
||||
file_token?: string
|
||||
filename: string
|
||||
platform: string
|
||||
source: string
|
||||
|
||||
Reference in New Issue
Block a user