diff options
| author | Julia <julia@insertdomain.name> | 2025-04-28 19:15:54 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-29 08:15:54 +0900 |
| commit | d10fdfe9738b17a9d81037c031b40a2cc4cb8038 (patch) | |
| tree | 54eab11525d76e242bd5bd5b7e95bef48267f6fb /packages | |
| parent | New Crowdin updates (#15897) (diff) | |
| download | sharkey-d10fdfe9738b17a9d81037c031b40a2cc4cb8038.tar.gz sharkey-d10fdfe9738b17a9d81037c031b40a2cc4cb8038.tar.bz2 sharkey-d10fdfe9738b17a9d81037c031b40a2cc4cb8038.zip | |
Merge commit from fork
* SP-2025-03.1 always wrap icon&thumbnail URLs
if they're not HTTP URLs, the frontend won't be able to display them
anyway (`<img src="mailto:…">` or '<div stile="background-image:
url(nntp:…)">` aren't going to work!), so let's always run them through the
media proxy, which will fail harder (fetching a `javascript:` URL
won't do anything in the backend, might do something in the frontend)
and will always protect the client's address in cases like `gemini:`
where the browser could try to fetch
* SP-2025-03.2 use object binding for more styles
interpolating a random (remote-controlled!) string into a `style`
attribute is a bad idea; using VueJS object binding, we should get
proper quoting and therefore safe parse failures instead of CSS
injections / XSS
* SP-2025-03.3 slightly more robust "self" URL handling
parse URLs instead of treating them as strings; this is still not
perfect, but the `URL` class only handles full URLs, not relative
ones, so there's so way to ask it "give me a URL object that
represents this resource relative to this base URL"
notice that passing very weird URLs to `MkUrl` and `MkUrlPreview` will
break the frontend (in dev mode) because there's an untrapped `new
URL(…)` that may explode; production builds seem to safely ignore the
error, though
---------
Co-authored-by: dakkar <dakkar@thenautilus.net>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/src/server/web/UrlPreviewService.ts | 10 | ||||
| -rw-r--r-- | packages/frontend-shared/js/url.ts | 17 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkLink.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUrlPreview.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserInfo.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserPopup.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUserSetupDialog.User.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/global/MkUrl.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/WidgetPhotos.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/widgets/server-metric/cpu-mem.vue | 4 |
10 files changed, 40 insertions, 19 deletions
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 9b5f0acd2c..531d085315 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -37,12 +37,10 @@ export class UrlPreviewService { @bindThis private wrap(url?: string | null): string | null { return url != null - ? url.match(/^https?:\/\//) - ? `${this.config.mediaProxy}/preview.webp?${query({ - url, - preview: '1', - })}` - : url + ? `${this.config.mediaProxy}/preview.webp?${query({ + url, + preview: '1', + })}` : null; } diff --git a/packages/frontend-shared/js/url.ts b/packages/frontend-shared/js/url.ts index eb830b1eea..e4f9ca513d 100644 --- a/packages/frontend-shared/js/url.ts +++ b/packages/frontend-shared/js/url.ts @@ -26,3 +26,20 @@ export function extractDomain(url: string) { const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); return match ? match[1] : null; } + +export function maybeMakeRelative(urlStr: string, baseStr: string): string { + try { + const baseObj = new URL(baseStr); + const urlObj = new URL(urlStr); + /* in all places where maybeMakeRelative is used, baseStr is the + * instance's public URL, which can't have path components, so the + * relative URL will always have the whole path from the urlStr + */ + if (urlObj.origin === baseObj.origin) { + return urlObj.pathname + urlObj.search + urlObj.hash; + } + return urlStr; + } catch (e) { + return ''; + } +} diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 4bac2bcea4..4cbf289448 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <component - :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target" + :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="maybeRelativeUrl" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" :title="url" > @@ -21,6 +21,7 @@ import { useTooltip } from '@/use/use-tooltip.js'; import * as os from '@/os.js'; import { isEnabledUrlPreview } from '@/instance.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; +import { maybeMakeRelative } from '@@/js/url.js'; const props = withDefaults(defineProps<{ url: string; @@ -29,7 +30,8 @@ const props = withDefaults(defineProps<{ }>(), { }); -const self = props.url.startsWith(local); +const maybeRelativeUrl = maybeMakeRelative(props.url, local); +const self = maybeRelativeUrl !== props.url; const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 20dab6f028..71c8a6a6e8 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -44,8 +44,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> <div v-else> - <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> - <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`"> + <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="maybeRelativeUrl" rel="nofollow noopener" :target="target" :title="url"> + <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : { backgroundImage: `url('${thumbnail}')` }"> </div> <article :class="$style.body"> <header :class="$style.header"> @@ -94,6 +94,7 @@ import MkButton from '@/components/MkButton.vue'; import { transformPlayerUrl } from '@/utility/player-url-transform.js'; import { store } from '@/store.js'; import { prefer } from '@/preferences.js'; +import { maybeMakeRelative } from '@@/js/url.js'; type SummalyResult = Awaited<ReturnType<typeof summaly>>; @@ -111,7 +112,8 @@ const props = withDefaults(defineProps<{ const MOBILE_THRESHOLD = 500; const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); -const self = props.url.startsWith(local); +const maybeRelativeUrl = maybeMakeRelative(props.url, local); +const self = maybeRelativeUrl !== props.url; const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; const fetching = ref(true); diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index cff531b2ca..69144d3824 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel" :class="$style.root"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> + <div :class="$style.banner" :style="user.bannerUrl ? { backgroundImage: `url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` } : ''"></div> <MkAvatar :class="$style.avatar" :user="user" indicator/> <div :class="$style.title"> <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 17a882a3a6..3bd2a2ffae 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="user != null"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> + <div :class="$style.banner" :style="user.bannerUrl ? { backgroundImage: `url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` } : ''"> <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> </div> <svg viewBox="0 0 128 128" :class="$style.avatarBack"> diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index 4accc6183b..54887ba0f0 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-adaptive-bg class="_panel" style="position: relative;"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> + <div :class="$style.banner" :style="user.bannerUrl ? { backgroundImage: `url(${user.bannerUrl})` } : ''"></div> <MkAvatar :class="$style.avatar" :user="user" indicator/> <div :class="$style.title"> <div :class="$style.name"><MkUserName :user="user" :nowrap="false"/></div> diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 0c248b041d..49f716d886 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <component - :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target" + :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="maybeRelativeUrl" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" @contextmenu.stop="() => {}" > @@ -32,6 +32,7 @@ import * as os from '@/os.js'; import { useTooltip } from '@/use/use-tooltip.js'; import { isEnabledUrlPreview } from '@/instance.js'; import type { MkABehavior } from '@/components/global/MkA.vue'; +import { maybeMakeRelative } from '@@/js/url.js'; function safeURIDecode(str: string): string { try { @@ -50,7 +51,8 @@ const props = withDefaults(defineProps<{ showUrlPreview: true, }); -const self = props.url.startsWith(local); +const maybeRelativeUrl = maybeMakeRelative(props.url, local); +const self = maybeRelativeUrl !== props.url; const url = new URL(props.url); if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); const el = ref(); diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 5d6e2ed48f..6bbc52b2c2 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-for="(image, i) in images" :key="i" :class="$style.img" - :style="`background-image: url(${thumbnail(image)})`" + :style="{ backgroundImage: `url(${thumbnail(image)})` }" ></div> </div> </div> diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index 469075e2c4..614ffe59e3 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only <rect x="-2" y="-2" :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`" + :style="{ stroke: 'none', fill: `url(#${ cpuGradientId })`, mask: `url(#${ cpuMaskId })` }" /> <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> </svg> @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only <rect x="-2" y="-2" :width="viewBoxX + 4" :height="viewBoxY + 4" - :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`" + :style="{ stroke: 'none', fill: `url(#${ memGradientId })`, mask: `url(#${ memMaskId })` }" /> <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> </svg> |