fix: 收紧Bilibili链接解析边界

This commit is contained in:
sunlei 2026-06-19 16:50:32 +08:00
parent 7522b4c2b2
commit 2d0d8dc4cd
2 changed files with 48 additions and 10 deletions

View File

@ -2,10 +2,10 @@ import type { BilibiliVideoReference } from './bilibili-card.types';
const BILIBILI_HOST = 'bilibili.com'; const BILIBILI_HOST = 'bilibili.com';
const B23_HOST = 'b23.tv'; const B23_HOST = 'b23.tv';
const TRAILING_WRAPPERS = /[\s"'<>)\]}]+$/u; const TRAILING_WRAPPERS = /[\s"'<>)\]}]+$/u;
const LEADING_WRAPPERS = /^[\s"'<>([{]+/u; const LEADING_WRAPPERS = /^[\s"'<>([{]+/u;
const BVID_PATTERN = /(?:^|\/)(BV[0-9A-Za-z]{10,})/; const BVID_PATTERN = /^BV[0-9A-Za-z]{10}$/;
const AID_PATTERN = /(?:^|\/)(?:av|AV)(\d+)(?:$|[/?#])/; const AID_PATTERN = /^(?:av|AV)(\d+)$/;
/** /**
* Removes QQ card wrappers and punctuation that often stick to copied URLs. * Removes QQ card wrappers and punctuation that often stick to copied URLs.
@ -17,6 +17,10 @@ export function cleanBilibiliUrlCandidate(candidate: string) {
.replaceAll('&amp;', '&') .replaceAll('&amp;', '&')
.replaceAll('&quot;', '"') .replaceAll('&quot;', '"')
.replaceAll('&#34;', '"') .replaceAll('&#34;', '"')
.replaceAll('&lt;', '<')
.replaceAll('&#60;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#62;', '>')
.replace(LEADING_WRAPPERS, '') .replace(LEADING_WRAPPERS, '')
.replace(TRAILING_WRAPPERS, '') .replace(TRAILING_WRAPPERS, '')
.trim(); .trim();
@ -54,18 +58,23 @@ export function parseBilibiliVideoReference(
if (!isAllowedBilibiliUrl(cleaned)) return null; if (!isAllowedBilibiliUrl(cleaned)) return null;
const url = new URL(cleaned); const url = new URL(cleaned);
const probe = `${url.pathname}${url.search}${url.hash}`; const pathSegments = url.pathname.split('/').filter(Boolean);
const bvid = probe.match(BVID_PATTERN)?.[1]; const videoSegmentIndex = pathSegments.findIndex(
if (bvid) { (segment) => segment.toLowerCase() === 'video',
);
if (videoSegmentIndex < 0) return null;
const videoIdSegment = pathSegments[videoSegmentIndex + 1];
if (videoIdSegment && BVID_PATTERN.test(videoIdSegment)) {
return { return {
canonicalVideoId: bvid, canonicalVideoId: videoIdSegment,
kind: 'bvid', kind: 'bvid',
sourceUrl: cleaned, sourceUrl: cleaned,
value: bvid, value: videoIdSegment,
}; };
} }
const aid = `${url.pathname}/`.match(AID_PATTERN)?.[1]; const aid = videoIdSegment?.match(AID_PATTERN)?.[1];
if (aid) { if (aid) {
return { return {
canonicalVideoId: `av${aid}`, canonicalVideoId: `av${aid}`,

View File

@ -44,4 +44,33 @@ describe('Bilibili URL parser', () => {
), ),
).toBe('https://www.bilibili.com/video/BV1xx411c7mD?p=1'); ).toBe('https://www.bilibili.com/video/BV1xx411c7mD?p=1');
}); });
it('does not parse video ids from query or hash on non-video Bilibili pages', () => {
expect(
parseBilibiliVideoReference(
'https://space.bilibili.com/1?from=/video/BV1xx411c7mD',
),
).toBeNull();
expect(
parseBilibiliVideoReference(
'https://www.bilibili.com/search?keyword=BV1xx411c7mD',
),
).toBeNull();
});
it('rejects malformed BV path segments with extra characters', () => {
expect(
parseBilibiliVideoReference(
'https://www.bilibili.com/video/BV1xx411c7mDextra',
),
).toBeNull();
});
it('cleans xml html entities and full-width brackets', () => {
expect(
cleanBilibiliUrlCandidate(
'&lt;【https://www.bilibili.com/video/BV1xx411c7mD】&gt;',
),
).toBe('https://www.bilibili.com/video/BV1xx411c7mD');
});
}); });