Merge pull request #737 from PicPeak/fix/auth-access-control

fix(security): cross-event thumbnail leak, bulk-op ownership bypass + auth hardening
This commit is contained in:
Paul Nothaft
2026-07-03 11:47:25 +02:00
committed by GitHub
22 changed files with 372 additions and 60 deletions
@@ -137,18 +137,26 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
throw new Error('No URL provided');
}
// Build full URL for the image
// Build full URL for the image. Only relative paths are app-owned;
// an absolute URL is passed through untouched.
const isRelative = rawUrl.startsWith('/');
const fullImageUrl = rawUrl.startsWith('/admin')
? buildResourceUrl(`/api${rawUrl}`)
: rawUrl.startsWith('/')
: isRelative
? buildResourceUrl(rawUrl)
: rawUrl;
const headers: Record<string, string> = {};
const slugForRequest = resolveSlug(rawUrl);
const token = getGalleryToken(slugForRequest);
if (token) {
headers.Authorization = `Bearer ${token}`;
// Attach the gallery bearer token ONLY to relative (same-app) image
// paths. Never send it to an absolute/external URL — that would leak
// gallery credentials cross-origin. AuthenticatedImage does not
// support external URLs by design.
if (isRelative) {
const slugForRequest = resolveSlug(rawUrl);
const token = getGalleryToken(slugForRequest);
if (token) {
headers.Authorization = `Bearer ${token}`;
}
}
const response = await fetch(fullImageUrl, {
+12 -4
View File
@@ -56,11 +56,19 @@ api.interceptors.request.use(
const pathname = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
const isGalleryEndpoint = /^\/gallery\//.test(pathname)
|| /^\/secure-images\//.test(pathname)
|| /^\/auth\/gallery\//.test(pathname);
// Never attach the gallery token to an absolute URL. Requests to the
// app's own API use relative paths (axios prepends baseURL); an
// absolute URL could point at any origin, and extracting its
// `/gallery/...` pathname would otherwise match below and leak the
// bearer token cross-origin.
const isAbsoluteUrl = /^https?:\/\//i.test(config.url || '');
const isGallerySessionCheck = pathname === '/auth/session'
const isGalleryEndpoint = !isAbsoluteUrl && (
/^\/gallery\//.test(pathname)
|| /^\/secure-images\//.test(pathname)
|| /^\/auth\/gallery\//.test(pathname));
const isGallerySessionCheck = !isAbsoluteUrl && pathname === '/auth/session'
&& (!!paramSlug || window.location.pathname.startsWith('/gallery/'));
if (isGalleryEndpoint || isGallerySessionCheck) {