mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(codebase audit): preserve cleanup without regressions
Fix codebase audit regressions while preserving UI/UX behavior and adding review-driven hardening.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import localForageLru from '@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js';
|
||||
import { canEmbed } from '../../components/embed';
|
||||
import memoize from 'memoizee';
|
||||
import { isValidURL } from './url-utils';
|
||||
import { isPrivateNetworkHostname, isValidURL, parseHttpUrl } from './url-utils';
|
||||
import { Capacitor, CapacitorHttp } from '@capacitor/core';
|
||||
|
||||
export interface CommentMediaInfo {
|
||||
@@ -15,7 +15,9 @@ export interface CommentMediaInfo {
|
||||
linkHeight?: number;
|
||||
}
|
||||
|
||||
export const getDisplayMediaInfoType = (type: string, t: any) => {
|
||||
type Translate = (key: string) => string;
|
||||
|
||||
export const getDisplayMediaInfoType = (type: string, t: Translate) => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return t('image');
|
||||
@@ -95,6 +97,23 @@ const isThumbnailDomainBlacklisted = (link: string | undefined): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseAllowedThumbnailFetchUrl = (value: string): URL | undefined => {
|
||||
const parsedUrl = parseHttpUrl(value);
|
||||
if (!parsedUrl || parsedUrl.protocol !== 'https:' || isPrivateNetworkHostname(parsedUrl.hostname)) {
|
||||
return undefined;
|
||||
}
|
||||
return parsedUrl;
|
||||
};
|
||||
|
||||
const getAllowedThumbnailUrl = (value: string, baseUrl: string): string | undefined => {
|
||||
try {
|
||||
const parsedUrl = new URL(value, baseUrl);
|
||||
return parsedUrl.protocol === 'https:' && !isPrivateNetworkHostname(parsedUrl.hostname) ? parsedUrl.href : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLinkMediaInfo = memoize(
|
||||
(link: string): CommentMediaInfo | undefined => {
|
||||
if (!isValidURL(link)) {
|
||||
@@ -148,6 +167,9 @@ export const getLinkMediaInfo = memoize(
|
||||
|
||||
const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> => {
|
||||
try {
|
||||
const parsedUrl = parseAllowedThumbnailFetchUrl(url);
|
||||
if (!parsedUrl) return undefined;
|
||||
|
||||
let html: string;
|
||||
const MAX_HTML_SIZE = 1024 * 1024;
|
||||
const TIMEOUT = 5000;
|
||||
@@ -155,10 +177,11 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
// in the native app, the Capacitor HTTP plugin is used to fetch the thumbnail
|
||||
const response = await CapacitorHttp.get({
|
||||
url,
|
||||
url: parsedUrl.href,
|
||||
readTimeout: TIMEOUT,
|
||||
connectTimeout: TIMEOUT,
|
||||
responseType: 'text',
|
||||
disableRedirects: true,
|
||||
headers: { Accept: 'text/html', Range: `bytes=0-${MAX_HTML_SIZE - 1}` },
|
||||
});
|
||||
html = response.data.slice(0, MAX_HTML_SIZE);
|
||||
@@ -167,8 +190,9 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch(parsedUrl.href, {
|
||||
signal: controller.signal,
|
||||
redirect: 'manual',
|
||||
headers: { Accept: 'text/html' },
|
||||
});
|
||||
|
||||
@@ -177,9 +201,10 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) return undefined;
|
||||
let result = '';
|
||||
while (true) {
|
||||
const { done, value } = await reader!.read();
|
||||
const { done, value } = await reader.read();
|
||||
if (done || result.length >= MAX_HTML_SIZE) break;
|
||||
result += new TextDecoder().decode(value);
|
||||
}
|
||||
@@ -191,14 +216,17 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
|
||||
|
||||
// Try to find Open Graph image
|
||||
const ogImage = doc.querySelector('meta[property="og:image"]');
|
||||
if (ogImage && ogImage.getAttribute('content')) {
|
||||
return ogImage.getAttribute('content')!;
|
||||
const ogImageContent = ogImage?.getAttribute('content');
|
||||
if (ogImageContent) {
|
||||
const ogImageUrl = getAllowedThumbnailUrl(ogImageContent, parsedUrl.href);
|
||||
if (ogImageUrl) return ogImageUrl;
|
||||
}
|
||||
|
||||
// If no Open Graph image, try to find the first image
|
||||
const firstImage = doc.querySelector('img');
|
||||
if (firstImage && firstImage.getAttribute('src')) {
|
||||
return new URL(firstImage.getAttribute('src')!, url).href;
|
||||
const firstImageSrc = firstImage?.getAttribute('src');
|
||||
if (firstImageSrc) {
|
||||
return getAllowedThumbnailUrl(firstImageSrc, parsedUrl.href);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -214,6 +242,7 @@ export const getCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidt
|
||||
}
|
||||
const linkInfo = link ? getLinkMediaInfo(link) : undefined;
|
||||
if (linkInfo) {
|
||||
const safeThumbnailUrl = thumbnailUrl ? getAllowedThumbnailUrl(thumbnailUrl, linkInfo.url) : undefined;
|
||||
// Don't show thumbnails for blacklisted domains (e.g., Twitter/X) as they return non-thumbnail images like emojis
|
||||
if (isThumbnailDomainBlacklisted(link)) {
|
||||
return {
|
||||
@@ -226,7 +255,7 @@ export const getCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidt
|
||||
}
|
||||
return {
|
||||
...linkInfo,
|
||||
thumbnail: thumbnailUrl || linkInfo.thumbnail,
|
||||
thumbnail: safeThumbnailUrl || linkInfo.thumbnail,
|
||||
linkWidth,
|
||||
linkHeight,
|
||||
};
|
||||
@@ -287,8 +316,9 @@ const setCachedThumbnail = async (url: string, thumbnail: string): Promise<void>
|
||||
export const fetchWebpageThumbnailIfNeeded = async (commentMediaInfo: CommentMediaInfo): Promise<CommentMediaInfo> => {
|
||||
if (commentMediaInfo.type === 'webpage' && !commentMediaInfo.thumbnail) {
|
||||
const cachedThumbnail = await getCachedThumbnail(commentMediaInfo.url);
|
||||
if (cachedThumbnail) {
|
||||
return { ...commentMediaInfo, thumbnail: cachedThumbnail };
|
||||
const safeCachedThumbnail = cachedThumbnail ? getAllowedThumbnailUrl(cachedThumbnail, commentMediaInfo.url) : undefined;
|
||||
if (safeCachedThumbnail) {
|
||||
return { ...commentMediaInfo, thumbnail: safeCachedThumbnail };
|
||||
}
|
||||
const thumbnail = await fetchWebpageThumbnail(commentMediaInfo.url);
|
||||
if (thumbnail) {
|
||||
|
||||
Reference in New Issue
Block a user