Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f7a8f64e2 |
@@ -43,7 +43,9 @@ export function SettingTile({ title, description, before, after, children }: Set
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{description && (
|
{description && (
|
||||||
<Text className={BreakWord} size="T200" priority="300">
|
// [Gitea #291] A <div>, not a <p>: many tiles pass blocks (badges,
|
||||||
|
// <Box>, <Text>) as the description, which is invalid inside a <p>.
|
||||||
|
<Text as="div" className={BreakWord} size="T200" priority="300">
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -310,9 +310,13 @@ export function UploadCardRenderer({
|
|||||||
const { file } = upload;
|
const { file } = upload;
|
||||||
const fileSizeExceeded = file.size >= allowSize;
|
const fileSizeExceeded = file.size >= allowSize;
|
||||||
|
|
||||||
if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
|
// [Gitea #291] Start in an effect, not during render: the desktop taskbar
|
||||||
startUpload();
|
// progress hook subscribes to the upload atoms, so a render-time start
|
||||||
}
|
// updated another component mid-render (React warning on every image send).
|
||||||
|
const idle = upload.status === UploadStatus.Idle;
|
||||||
|
useEffect(() => {
|
||||||
|
if (idle && !fileSizeExceeded) startUpload();
|
||||||
|
}, [idle, fileSizeExceeded, startUpload]);
|
||||||
|
|
||||||
const handleSpoiler = (marked: boolean) => {
|
const handleSpoiler = (marked: boolean) => {
|
||||||
setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
|
setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
GIF_PREVIEW_MAX_WIDTH,
|
GIF_PREVIEW_MAX_WIDTH,
|
||||||
gifPreviewBox,
|
gifPreviewBox,
|
||||||
} from '../../utils/gifPreviewSize';
|
} from '../../utils/gifPreviewSize';
|
||||||
import { sameProvider } from './embedProvider';
|
|
||||||
|
|
||||||
const linkStyles = { color: color.Success.Main };
|
const linkStyles = { color: color.Success.Main };
|
||||||
|
|
||||||
@@ -2498,10 +2497,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
const resolveEmbed = (prev: IPreviewUrlResponse): MediaEmbed | null => {
|
const resolveEmbed = (prev: IPreviewUrlResponse): MediaEmbed | null => {
|
||||||
if (embed) return embed;
|
if (embed) return embed;
|
||||||
const ogUrl = prev['og:url'];
|
const ogUrl = prev['og:url'];
|
||||||
// [Gitea #326] …but only from an og:url on the posted link's own
|
return typeof ogUrl === 'string' && ogUrl !== url
|
||||||
// provider: the page sets og:url, so any site could otherwise show up
|
|
||||||
// as a playable YouTube (etc.) card that links to itself.
|
|
||||||
return typeof ogUrl === 'string' && ogUrl !== url && sameProvider(url, ogUrl)
|
|
||||||
? parseMediaEmbed(ogUrl, window.location.hostname)
|
? parseMediaEmbed(ogUrl, window.location.hostname)
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { test } from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { sameProvider } from './embedProvider';
|
|
||||||
|
|
||||||
test("sameProvider: a page cannot borrow another provider's embed via og:url (#326)", () => {
|
|
||||||
const yt = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
|
||||||
assert.equal(sameProvider('https://evil.example/free-gift', yt), false);
|
|
||||||
assert.equal(sameProvider('https://youtube.com.evil.example/x', yt), false);
|
|
||||||
assert.equal(sameProvider('https://youtu.be/abc', yt), true);
|
|
||||||
assert.equal(sameProvider('https://m.youtube.com/shorts/abc', yt), true);
|
|
||||||
assert.equal(
|
|
||||||
sameProvider('https://vm.tiktok.com/ZM123/', 'https://www.tiktok.com/@a/video/1'),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(sameProvider('https://spotify.link/abc', 'https://open.spotify.com/track/1'), true);
|
|
||||||
assert.equal(sameProvider('https://on.soundcloud.com/x', 'https://soundcloud.com/a/b'), true);
|
|
||||||
assert.equal(
|
|
||||||
sameProvider('https://redd.it/abc', 'https://www.reddit.com/r/x/comments/abc/'),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(sameProvider('not a url', yt), false);
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* [Gitea #326] A page's og:url is chosen by the page itself, so a phishing
|
|
||||||
* link could serve `og:url = youtube.com/watch?v=…` and render as a playable
|
|
||||||
* YouTube card linking to the attacker. Only trust an og:url on the same
|
|
||||||
* provider as the link actually posted (including the provider's own
|
|
||||||
* short-link domains).
|
|
||||||
*/
|
|
||||||
const PROVIDER_ALIASES: Record<string, string> = {
|
|
||||||
'youtu.be': 'youtube.com',
|
|
||||||
'youtube-nocookie.com': 'youtube.com',
|
|
||||||
'spotify.link': 'spotify.com',
|
|
||||||
'redd.it': 'reddit.com',
|
|
||||||
'x.com': 'twitter.com',
|
|
||||||
'twitch.tv': 'twitch.tv',
|
|
||||||
'apple.co': 'apple.com',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure: the provider a host belongs to (last two labels, aliases folded). Hosts
|
|
||||||
* under two-part public suffixes (`*.co.uk`) share a value, which is harmless
|
|
||||||
* here: no embeddable provider lives under one.
|
|
||||||
*/
|
|
||||||
export const providerOf = (host: string): string => {
|
|
||||||
const parts = host.toLowerCase().replace(/\.$/, '').split('.');
|
|
||||||
const base = parts.slice(-2).join('.');
|
|
||||||
return PROVIDER_ALIASES[base] ?? base;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Pure: whether `ogUrl` is on the same provider as the posted `url`. */
|
|
||||||
export function sameProvider(url: string, ogUrl: string): boolean {
|
|
||||||
try {
|
|
||||||
return providerOf(new URL(url).hostname) === providerOf(new URL(ogUrl).hostname);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user