🔒️(frontend) harden security check on url

We harden the security check on url to prevent attacks.
This commit is contained in:
Anthony LC
2026-02-05 17:24:04 +01:00
parent b8c1504e7a
commit d02c6250c9
7 changed files with 23 additions and 11 deletions

View File

@@ -1,5 +1,8 @@
import { APIError, errorCauses } from '@/api'; import { APIError, errorCauses } from '@/api';
import { sleep } from '@/utils'; import { sleep } from '@/utils';
import { isSafeUrl } from '@/utils/url';
import { ANALYZE_URL } from '../conf';
interface CheckDocMediaStatusResponse { interface CheckDocMediaStatusResponse {
file?: string; file?: string;
@@ -13,6 +16,10 @@ interface CheckDocMediaStatus {
export const checkDocMediaStatus = async ({ export const checkDocMediaStatus = async ({
urlMedia, urlMedia,
}: CheckDocMediaStatus): Promise<CheckDocMediaStatusResponse> => { }: CheckDocMediaStatus): Promise<CheckDocMediaStatusResponse> => {
if (!isSafeUrl(urlMedia) || !urlMedia.includes(ANALYZE_URL)) {
throw new APIError('Url invalid', { status: 400 });
}
const response = await fetch(urlMedia, { const response = await fetch(urlMedia, {
credentials: 'include', credentials: 'include',
}); });

View File

@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
import { Box, Icon, Loading } from '@/components'; import { Box, Icon, Loading } from '@/components';
import { isSafeUrl } from '@/utils/url';
import Warning from '../../assets/warning.svg'; import Warning from '../../assets/warning.svg';
import { ANALYZE_URL } from '../../conf'; import { ANALYZE_URL } from '../../conf';
@@ -87,7 +88,7 @@ const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
}, [lang, t]); }, [lang, t]);
useEffect(() => { useEffect(() => {
if (!pdfUrl || pdfUrl.includes(ANALYZE_URL)) { if (!pdfUrl || pdfUrl.includes(ANALYZE_URL) || !isSafeUrl(pdfUrl)) {
return; return;
} }
@@ -115,7 +116,8 @@ const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
}, [pdfUrl]); }, [pdfUrl]);
const isInvalidPDF = const isInvalidPDF =
!isPDFContentLoading && isPDFContent !== null && !isPDFContent; (!isPDFContentLoading && isPDFContent !== null && !isPDFContent) ||
!isSafeUrl(pdfUrl);
if (isInvalidPDF) { if (isInvalidPDF) {
return ( return (

View File

@@ -11,6 +11,7 @@ import { useEffect } from 'react';
import { Box, Text } from '@/components'; import { Box, Text } from '@/components';
import { useMediaUrl } from '@/core'; import { useMediaUrl } from '@/core';
import { isSafeUrl } from '@/utils/url';
import { loopCheckDocMediaStatus } from '../../api'; import { loopCheckDocMediaStatus } from '../../api';
import Loader from '../../assets/loader.svg'; import Loader from '../../assets/loader.svg';
@@ -67,7 +68,7 @@ const UploadLoaderBlockComponent = ({
block.props.type === 'loading' && block.props.type === 'loading' &&
isEditable; isEditable;
if (!shouldCheckStatus) { if (!shouldCheckStatus || !isSafeUrl(block.props.blockUploadUrl)) {
return; return;
} }

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { backendUrl } from '@/api'; import { backendUrl } from '@/api';
import { isSafeUrl } from '@/utils/url';
import { useCreateDocAttachment } from '../api'; import { useCreateDocAttachment } from '../api';
import { ANALYZE_URL } from '../conf'; import { ANALYZE_URL } from '../conf';
@@ -57,7 +58,8 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
if ( if (
!block || !block ||
!('url' in block.props) || !('url' in block.props) ||
('url' in block.props && !block.props.url.includes(ANALYZE_URL)) ('url' in block.props && !block.props.url.includes(ANALYZE_URL)) ||
!isSafeUrl(block.props.url)
) { ) {
return; return;
} }

View File

@@ -1,5 +1,7 @@
import JSZip from 'jszip'; import JSZip from 'jszip';
import { isSafeUrl } from '@/utils/url';
import { exportResolveFileUrl } from './api'; import { exportResolveFileUrl } from './api';
// Escape user-provided text before injecting it into the exported HTML document. // Escape user-provided text before injecting it into the exported HTML document.
@@ -414,7 +416,7 @@ export const addMediaFilesToZip = async (
url = null; url = null;
} }
if (!url || url.origin !== mediaUrl) { if (!url || url.origin !== mediaUrl || !isSafeUrl(url.href)) {
return; return;
} }

View File

@@ -13,9 +13,7 @@ const getDocVersion = async ({
versionId, versionId,
docId, docId,
}: DocVersionParam): Promise<Version> => { }: DocVersionParam): Promise<Version> => {
const url = `documents/${docId}/versions/${versionId}/`; const response = await fetchAPI(`documents/${docId}/versions/${versionId}/`);
const response = await fetchAPI(url);
if (!response.ok) { if (!response.ok) {
throw new APIError( throw new APIError(

View File

@@ -29,9 +29,9 @@ const getDocVersions = async ({
versionId, versionId,
docId, docId,
}: DocVersionsAPIParams): Promise<VersionsResponse> => { }: DocVersionsAPIParams): Promise<VersionsResponse> => {
const url = `documents/${docId}/versions/?version_id=${versionId}`; const response = await fetchAPI(
`documents/${docId}/versions/?version_id=${versionId}`,
const response = await fetchAPI(url); );
if (!response.ok) { if (!response.ok) {
throw new APIError( throw new APIError(