🛡️ Sentinel: [HIGH] Fix timingSafeEqual DoS Vulnerability (#423)

* Fix timingSafeEqual DoS Vulnerability in Webhook

Co-authored-by: ralksta <16941050+ralksta@users.noreply.github.com>

* Fix memory exhaustion DoS in token decoding

Co-authored-by: ralksta <16941050+ralksta@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-08-14 11:10:42 +02:00
committed by GitHub
parent f120f05157
commit d4d8c8b42f
20 changed files with 1574 additions and 221 deletions

View File

@@ -104,6 +104,77 @@ describe('deriveGallery accepts valid structures', () => {
expect(result.albumHeroImages[A]).toBe(B);
});
it('collects the per-album sort mode', () => {
const result = deriveGallery({
albums: [{ [A]: { title: 'Series', sort: 'filename' } }, B],
} as unknown as GalleryYaml);
expect(result.albumSortModes[A]).toBe('filename');
// Absent means the default; an explicit entry would make `immich` and
// "never configured" indistinguishable downstream.
expect(result.albumSortModes[B]).toBeUndefined();
});
// A typo would otherwise be invisible: the album would keep rendering in
// Immich order and nothing would say why. Throwing is also what gives the
// admin PUT its 400, via the deriveGallery dry-run.
it('rejects an unknown sort mode, naming the album and the valid values', () => {
expect(() =>
deriveGallery({ albums: [{ [A]: { sort: 'alphabetical' } }] } as unknown as GalleryYaml),
).toThrow(/alphabetical/);
expect(() =>
deriveGallery({ albums: [{ [A]: { sort: 'alphabetical' } }] } as unknown as GalleryYaml),
).toThrow(new RegExp(A));
expect(() =>
deriveGallery({ albums: [{ [A]: { sort: 'alphabetical' } }] } as unknown as GalleryYaml),
).toThrow(/filename/);
});
it('collects the manual asset order, preserving its order', () => {
const result = deriveGallery({
albums: [{ [A]: { sort: 'manual', assetOrder: [B, A] } }],
} as unknown as GalleryYaml);
expect(result.albumManualOrders[A]).toEqual([B, A]);
});
// Kept regardless of the mode, so toggling manual → newest → manual in the
// admin panel does not silently destroy a hand-curated order.
it('keeps the asset order even when the mode is not manual', () => {
const result = deriveGallery({
albums: [{ [A]: { sort: 'newest', assetOrder: [B] } }],
} as unknown as GalleryYaml);
expect(result.albumSortModes[A]).toBe('newest');
expect(result.albumManualOrders[A]).toEqual([B]);
});
it('ignores an empty asset order rather than storing one', () => {
const result = deriveGallery({
albums: [{ [A]: { sort: 'manual', assetOrder: [] } }],
} as unknown as GalleryYaml);
expect(result.albumManualOrders[A]).toBeUndefined();
});
// processAlbumEntry is reached from four places; only the array-subpage path
// was covered before, so a regression in the others would go unnoticed.
it('applies the sort mode to section and map-style subpage albums too', () => {
const sectioned = deriveGallery({
subpages: [
{ name: 'Work', sections: [{ title: 'Recent', albums: [{ [A]: { sort: 'oldest' } }] }] },
],
} as unknown as GalleryYaml);
expect(sectioned.albumSortModes[A]).toBe('oldest');
const mapStyle = deriveGallery({
subpages: { Trips: { albums: [{ [B]: { sort: 'filename' } }] } },
} as unknown as GalleryYaml);
expect(mapStyle.albumSortModes[B]).toBe('filename');
});
it('parses enabled property on subpages correctly', () => {
const result = deriveGallery({
subpages: [