fix(builder): reject external library paths that collide with /data (#777)

This commit is contained in:
bo0tzz
2026-09-03 19:08:34 +02:00
committed by GitHub
parent e609882b1c
commit c48faf1ede
2 changed files with 49 additions and 4 deletions
@@ -152,6 +152,42 @@ describe('validate', () => {
}
});
it('refuses an external library that lands inside the container upload directory', () => {
const config = structuredClone(DEFAULT_CONFIG);
for (const path of ['/data', '/data/library', '/data/upload', '/data/thumbs', '/data/anything']) {
config.storage.externalLibraries = [{ path, readOnly: true }];
expect(validate(config)['storage.externalLibraries.0.path'], path).toContain('/data');
}
});
it('refuses the filesystem root as an external library', () => {
const config = structuredClone(DEFAULT_CONFIG);
for (const path of ['/', '//']) {
config.storage.externalLibraries = [{ path, readOnly: true }];
expect(validate(config)['storage.externalLibraries.0.path'], path).toContain('root');
}
});
it('flags an external library that reuses an override host path', () => {
const config = structuredClone(DEFAULT_CONFIG);
config.storage.customFolders = true;
config.storage.overrides.thumbs = '/mnt/thumbs';
config.storage.externalLibraries = [{ path: '/mnt/thumbs', readOnly: true }];
const errors = validate(config);
expect(errors['storage.overrides.thumbs']).toBeTruthy();
expect(errors['storage.externalLibraries.0.path']).toBeTruthy();
});
it('still accepts an ordinary external library alongside overrides', () => {
const config = structuredClone(DEFAULT_CONFIG);
config.storage.customFolders = true;
config.storage.overrides.thumbs = '/mnt/fast/thumbs';
config.storage.externalLibraries = [{ path: '/mnt/media/photos', readOnly: true }];
expect(validate(config)).toEqual({});
});
it('flags an external library that overlaps another mount', () => {
const config = structuredClone(DEFAULT_CONFIG);
config.storage.uploadLocation = '/mnt/media';
+13 -4
View File
@@ -264,6 +264,8 @@ type ValidationErrors = Record<string, string>;
const normalizePath = (path: string) => path.trim().replace(/\/+$/, '');
const UPLOAD_TARGET = '/data';
type Mount = { path: string[]; location: string };
const collectMounts = (config: ImmichConfig): Mount[] => {
@@ -301,11 +303,18 @@ const validationSchema = immichConfigSchema.superRefine((config, ctx) => {
}
for (const [index, { path }] of config.storage.externalLibraries.entries()) {
const location = path.trim();
if (!location) {
fail(['storage', 'externalLibraries', String(index), 'path'], 'A path is required.');
const field = ['storage', 'externalLibraries', String(index), 'path'];
const trimmed = path.trim();
const location = normalizePath(path);
if (!trimmed) {
fail(field, 'A path is required.');
} else if (!location) {
fail(field, 'The filesystem root cannot be mounted as a library.');
} else if (!location.startsWith('/')) {
fail(['storage', 'externalLibraries', String(index), 'path'], 'Enter an absolute path, e.g. /mnt/media/photos.');
fail(field, 'Enter an absolute path, e.g. /mnt/media/photos.');
} else if (location === UPLOAD_TARGET || location.startsWith(`${UPLOAD_TARGET}/`)) {
fail(field, `Immich manages ${UPLOAD_TARGET} inside the container, so a library cannot live there.`);
}
}