Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/test/nginx-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ test('every policy in the config states a frame-ancestors', () => {
/* Google Fonts was allowed by three policies for a widget that no longer
exists, so every page could pull a stylesheet and a font from a host nothing
asked for. The app uses system fonts and ships its own assets. */
test('no policy lets a page connect to an outside host', () => {
const all = [...dashboard.matchAll(/add_header Content-Security-Policy "([^"]+)"/g)].map(m => m[1]);
all.push(policy);
for (const p of all) {
const connect = p
.split(';')
.map(d => d.trim())
.find(d => d.startsWith('connect-src '));
if (connect) assert.equal(connect, "connect-src 'self'");
}
});

test('no policy allows an outside font or stylesheet host', () => {
const all = [...dashboard.matchAll(/add_header Content-Security-Policy "([^"]+)"/g)].map(m => m[1]);
all.push(policy);
Expand Down
36 changes: 36 additions & 0 deletions e2e/icon-privacy.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @ts-check
/* Icons from the catalogues load through the server. The browser never asks
the CDN, even when the server cannot supply a file. */

const { test, expect } = require('@playwright/test');
const { seedConfig, dismissSetupPrompt, app, openDashboardList } = require('./helpers');

const ITEMS = [
{ ...app('unknown', 'Unknown'), iconUrl: 'no-such-icon-in-any-catalogue' },
{ ...app('linked', 'Linked'), iconUrl: 'https://cdn.jsdelivr.net/gh/selfhst/icons/svg/jellyfin-light.svg' },
];

test.beforeEach(async ({ request }) => {
await seedConfig(request, { items: ITEMS });
await dismissSetupPrompt(request);
});

for (const [path, open] of [
['/', page => page.goto('/')],
['/admin/', openDashboardList],
]) {
test(`${path} never requests an icon from the CDN`, async ({ page }) => {
const outside = [];
page.on('request', r => {
if (new URL(r.url()).host === 'cdn.jsdelivr.net') outside.push(r.url());
});
const proxied = [];
page.on('response', r => {
if (r.url().includes('/api/icons/cdn?')) proxied.push(new URL(r.url()).searchParams.get('name'));
});
await open(page);
await expect.poll(() => [...new Set(proxied)].sort()).toEqual(['jellyfin-light', 'no-such-icon-in-any-catalogue']);
await page.waitForLoadState('networkidle');
expect(outside).toEqual([]);
});
}
2 changes: 1 addition & 1 deletion nginx/csp-default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
#
# Keep this to the one header. Anything that differs per call site belongs in
# the location.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://images.unsplash.com https://cdn.jsdelivr.net; frame-src 'self'; frame-ancestors 'self'; connect-src 'self' https://api.unsplash.com https://cdn.jsdelivr.net; font-src 'self';" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://images.unsplash.com https://cdn.jsdelivr.net; frame-src 'self'; frame-ancestors 'self'; connect-src 'self'; font-src 'self';" always;
2 changes: 1 addition & 1 deletion nginx/dashboard.conf
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ server {
include /etc/nginx/http.d/security-headers.conf;
add_header X-Frame-Options "DENY" always;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://images.unsplash.com https://cdn.jsdelivr.net; connect-src 'self' https://cdn.jsdelivr.net; font-src 'self'; frame-ancestors 'none';" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https://images.unsplash.com https://cdn.jsdelivr.net; connect-src 'self'; font-src 'self'; frame-ancestors 'none';" always;
}

# ── Icons ──
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,6 @@ <h1 class="sec-title" id="ev-title">General</h1>
</div></div>
</div>

<script type="module" src="/js/admin.js?v=bb2719cd"></script>
<script type="module" src="/js/admin.js?v=2aaed21c"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@
</div>
</div>
</dialog>
<script type="module" src="/js/dashboard.js?v=25d81400"></script>
<script type="module" src="/js/dashboard.js?v=cec3f699"></script>
</body></html>
23 changes: 15 additions & 8 deletions ui/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=14b18b8c';
import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=dc4ac52e';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c7b5111';
import {
loadLocalIcons,
resolveIcon,
iconChain,
cdnIconRef,
splitIconRef,
throughServer,
} from '/js/icons.js?v=5ac01d72';
import { state } from '/js/admin-state.js?v=af772a1b';
import { DOCK_MAX } from '/js/limits.js?v=31048a24';
import { formatNumber } from '/js/format-number.js?v=abcd4ae4';
Expand All @@ -24,10 +31,10 @@ import {
reveal,
setTogDisabled,
swapContent,
} from '/js/admin-shared.js?v=36dc5cc1';
import { createListbox } from '/js/listbox.js?v=23e87940';
} from '/js/admin-shared.js?v=da7df247';
import { createListbox } from '/js/listbox.js?v=4afb5969';
import { MAX_LABELS } from '/js/badge-logic.js?v=5c72bd4c';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=e80f4a68';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=82916010';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { iconSvg } from '/js/icon-set.js?v=628341d4';
Expand Down Expand Up @@ -121,7 +128,7 @@ export function buildAppForm(body, item) {
<div class="grp" id="ipw">
<div class="icon-src-anchor">
<div class="row icon-src-row">
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${resolveIcon(state.siurl)}" alt="" id="ipv-img">` : html`<span>${initial(item?.label)}</span>`}</span>
<span class="icon-prev" id="ipv">${state.siurl ? html`<img src="${throughServer(resolveIcon(state.siurl))}" alt="" id="ipv-img">` : html`<span>${initial(item?.label)}</span>`}</span>
<input class="icon-srch" id="ip-in" type="text" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="iprs" aria-autocomplete="list" aria-describedby="ip-status" aria-label="${t('app.icon')}" placeholder="${t('app.iconPh')}" value="${state.siurl}">
<button type="button" class="row-btn" id="ip-upload-lbl">${t('app.upload')}</button>
<input type="file" id="ip-upload" class="file-hidden" tabindex="-1" aria-hidden="true" accept=".svg,.png,.ico,image/svg+xml,image/png,image/x-icon">
Expand Down Expand Up @@ -767,7 +774,7 @@ function ipRow(i, iconRef, name, source, urls = [], variants = []) {
const img = document.createElement('img');
img.alt = '';
/* Ordered by the catalogue, whose first entry is the file it actually holds. */
const srcs = [...urls, ...variants.flatMap(v => v.urls || [])].filter(Boolean);
const srcs = [...urls, ...variants.flatMap(v => v.urls || [])].filter(Boolean).map(throughServer);
if (!srcs.length) srcs.push(...iconChain(iconRef));
let step = 0;
img.src = srcs[0] || '';
Expand Down Expand Up @@ -857,7 +864,7 @@ function updPrev(explicit) {
setInitialGlyph(p);
return;
}
const fallbacks = explicit?.length ? explicit : iconChain(state.siurl);
const fallbacks = explicit?.length ? explicit.map(throughServer) : iconChain(state.siurl);
if (!fallbacks.length) {
setInitialGlyph(p);
return;
Expand Down
4 changes: 2 additions & 2 deletions ui/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiGet, apiPost } from '/js/admin-shared.js?v=36dc5cc1';
import { apiGet, apiPost } from '/js/admin-shared.js?v=da7df247';
import { initI18n, t } from '/js/i18n.js?v=ed7c5565';
import { loginErrorKey } from '/js/admin-error.js?v=b0881957';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { el, inp as inpById, qa } from '/js/utils.js?v=14b18b8c';
import { el, inp as inpById, qa } from '/js/utils.js?v=dc4ac52e';
import { blockingScreenFor } from '/js/config-recovery.js?v=85138697';

export async function checkAuth(onLogin) {
Expand Down
4 changes: 2 additions & 2 deletions ui/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=36dc5cc1';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=da7df247';
import { t } from '/js/i18n.js?v=ed7c5565';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=14b18b8c';
import { qa, q } from '/js/utils.js?v=dc4ac52e';
import { iconSvg } from '/js/icon-set.js?v=628341d4';
import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43';

Expand Down
2 changes: 1 addition & 1 deletion ui/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa';
import { qa } from '/js/utils.js?v=14b18b8c';
import { qa } from '/js/utils.js?v=dc4ac52e';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
8 changes: 4 additions & 4 deletions ui/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { snapshotItems } from '/js/admin-save-logic.js?v=5613a9fa';
import { reorderItems } from '/js/admin-logic.js?v=fc7f0836';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=d9f5ad98';
import { paintIcon } from '/js/admin-shared.js?v=36dc5cc1';
import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=14b18b8c';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=29e09aad';
import { paintIcon } from '/js/admin-shared.js?v=da7df247';
import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=dc4ac52e';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=ed7c5565';
import { sizeLabel } from '/js/admin-widget-form.js?v=8a6be17f';
import { sizeLabel } from '/js/admin-widget-form.js?v=047fef40';
import { widgetGlyph } from '/js/widget-glyphs.js?v=b383b7c8';
import { iconSvg } from '/js/icon-set.js?v=628341d4';

Expand Down
8 changes: 4 additions & 4 deletions ui/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toast, apiGet, apiPost, errorText, reveal, swapContent, wireInlineEdit } from '/js/admin-shared.js?v=36dc5cc1';
import { toast, apiGet, apiPost, errorText, reveal, swapContent, wireInlineEdit } from '/js/admin-shared.js?v=da7df247';
import { socketProbeAdvice } from '/js/admin-error.js?v=b0881957';
import { pwStrength } from '/js/password-strength.js?v=389e0ed0';
import { t } from '/js/i18n.js?v=ed7c5565';
Expand All @@ -11,11 +11,11 @@ import {
BLOCK,
} from '/js/admin-logic.js?v=fc7f0836';
import { confirmText, promptModal } from '/js/modal.js?v=6b0320bd';
import { el, inp, setUserText } from '/js/utils.js?v=14b18b8c';
import { el, inp, setUserText } from '/js/utils.js?v=dc4ac52e';
import { formatNumber } from '/js/format-number.js?v=abcd4ae4';
import { serialWrites } from '/js/admin-save-logic.js?v=5613a9fa';
import { renderColorControl } from '/js/admin-color-control.js?v=e80f4a68';
import { BACKDROP } from '/js/background.js?v=0f4ca726';
import { renderColorControl } from '/js/admin-color-control.js?v=82916010';
import { BACKDROP } from '/js/background.js?v=d6f2e701';
import {
ALLOWED_HOSTS_MAX,
firstBadHost,
Expand Down
4 changes: 2 additions & 2 deletions ui/js/admin-shared.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* Stateless helpers shared by the admin modules. Mutable state stays out. */
import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=fc7f0836';
import { el, isolate, q } from '/js/utils.js?v=14b18b8c';
import { el, isolate, q } from '/js/utils.js?v=dc4ac52e';
import { t } from '/js/i18n.js?v=ed7c5565';
import { errorAdvice } from '/js/admin-error.js?v=b0881957';
import { formatList, formatNumber } from '/js/format-number.js?v=abcd4ae4';
import { iconChain } from '/js/icons.js?v=9c7b5111';
import { iconChain } from '/js/icons.js?v=5ac01d72';
import { iconSvg } from '/js/icon-set.js?v=628341d4';
import { blockingScreenFor, recoveryShown, showBlockingScreen } from '/js/config-recovery.js?v=85138697';

Expand Down
8 changes: 4 additions & 4 deletions ui/js/admin-widget-form.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { state } from '/js/admin-state.js?v=af772a1b';
import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=36dc5cc1';
import { createListbox } from '/js/listbox.js?v=23e87940';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=191cb80f';
import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=da7df247';
import { createListbox } from '/js/listbox.js?v=4afb5969';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=c4755d57';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=fc7f0836';
import { t } from '/js/i18n.js?v=ed7c5565';
import { focusFirst, q, qi, qa } from '/js/utils.js?v=14b18b8c';
import { focusFirst, q, qi, qa } from '/js/utils.js?v=dc4ac52e';
import { iconSvg } from '/js/icon-set.js?v=628341d4';

const SIZES_WITH_ICONS = new Set(['small', 'medium', 'large', 'xlarge']);
Expand Down
20 changes: 10 additions & 10 deletions ui/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=dd5f148f';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=71f8a57a';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=92c6949c';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=73ed7df1';
import { recoveryShown } from '/js/config-recovery.js?v=85138697';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=7b21e1a0';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=02db1758';
import { resolveAdminSection } from '/js/admin-logic.js?v=fc7f0836';
import {
afterImport,
Expand All @@ -23,7 +23,7 @@ import {
showBgFields,
showWallpaperFile,
wireUnsplashKey,
} from '/js/admin-settings.js?v=5d039019';
} from '/js/admin-settings.js?v=9ffe0860';
import {
apiGet,
apiPost,
Expand All @@ -37,16 +37,16 @@ import {
setReauthHandler,
toast,
withheldText,
} from '/js/admin-shared.js?v=36dc5cc1';
} from '/js/admin-shared.js?v=da7df247';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=8a6be17f';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=047fef40';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { formatNumber } from '/js/format-number.js?v=abcd4ae4';
import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0';
import { createListbox } from '/js/listbox.js?v=23e87940';
import { createListbox } from '/js/listbox.js?v=4afb5969';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=ed7c5565';
import { loadLocalIcons } from '/js/icons.js?v=9c7b5111';
import { loadLocalIcons } from '/js/icons.js?v=5ac01d72';
import { ensureSprite, iconSvg } from '/js/icon-set.js?v=628341d4';
import {
clearSkipTls,
Expand Down Expand Up @@ -82,8 +82,8 @@ import {
storeGet,
storeSet,
tgt,
} from '/js/utils.js?v=14b18b8c';
import { applyBackground, resolveBackground } from '/js/background.js?v=0f4ca726';
} from '/js/utils.js?v=dc4ac52e';
import { applyBackground, resolveBackground } from '/js/background.js?v=d6f2e701';
import { parseYamlTolerant, YamlLiteError, YamlTooLargeError } from '/js/yaml-lite.js?v=a42ef914';

ensureSprite();
Expand Down
2 changes: 1 addition & 1 deletion ui/js/background.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-check
import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=14b18b8c';
import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=dc4ac52e';
import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6';

export const BACKDROP = '#0d1117';
Expand Down
10 changes: 5 additions & 5 deletions ui/js/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadLocalIcons, iconChain } from '/js/icons.js?v=9c7b5111';
import { loadLocalIcons, iconChain } from '/js/icons.js?v=5ac01d72';
import {
WIDGET_HEIGHTS,
WIDGET_DESIGN,
Expand Down Expand Up @@ -28,9 +28,9 @@ import {
storeSet,
teardownWidgets,
titleWhenTruncated,
} from '/js/utils.js?v=14b18b8c';
} from '/js/utils.js?v=dc4ac52e';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { initSpotlight } from '/js/spotlight.js?v=97143da4';
import { initSpotlight } from '/js/spotlight.js?v=c76fb899';
import { html, setHtml, raw } from '/js/html.js?v=c71f8903';
import { initI18n, t, currentLang } from '/js/i18n.js?v=ed7c5565';
import { blockingScreenFor, showBlockingScreen } from '/js/config-recovery.js?v=85138697';
Expand All @@ -46,7 +46,7 @@ import {
buildMobile,
resetMobileChrome,
mkFolderGlyph,
} from '/js/ui.js?v=6af083c8';
} from '/js/ui.js?v=4265d299';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=5c72bd4c';
import { formatNumber } from '/js/format-number.js?v=abcd4ae4';
import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=23793736';
Expand All @@ -59,7 +59,7 @@ import {
restorePage,
wheelPager,
} from '/js/dashboard-logic.js?v=6a29cedc';
import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=0f4ca726';
import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=d6f2e701';
import { repeatJittered } from '/js/jitter.js?v=087a1fcf';
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607';
import { startWakeLock } from '/js/wake-lock.js?v=6b9591cf';
Expand Down
Loading
Loading