Skip to content

Html #4931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: test-html
Choose a base branch
from
Open

Html #4931

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docSite/content/zh-cn/docs/development/openapi/dataset.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ data 为集合的 ID。
{{< /tab >}}
{{< /tabs >}}

### 创建一个外部文件库集合(商业版
### 创建一个外部文件库集合(弃用

{{< tabs tabTotal="3" >}}
{{< tab tabName="请求示例" >}}
Expand Down
62 changes: 31 additions & 31 deletions projects/app/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { i18n } = require('./next-i18next.config.js');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

const isDev = process.env.NODE_ENV === 'development';

Expand All @@ -14,51 +15,50 @@ const nextConfig = {

headers: async () => {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = `'nonce-${nonce}'`;
const csp_nonce = `'nonce-${nonce}'`;
const scheme_source = 'data: mediastream: blob: filesystem:';
const NECESSARY_DOMAINS = [
'*.sentry.io',
'http://localhost:*',
'http://127.0.0.1:*',
'https://analytics.google.com',
'googletagmanager.com',
'*.googletagmanager.com',
'https://www.google-analytics.com',
'https://api.github.com'
].join(' ');

const SENTRY_DOMAINS = '*.sentry.io';
const GOOGLE_DOMAINS = 'https://www.googletagmanager.com https://www.google-analytics.com';
const LOCALHOST = 'http://localhost:* http://127.0.0.1:*';
const OTHER_DOMAINS = 'https://api.example.com';

const csp = [
`default-src 'self' ${scheme_source} ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST}`,
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${csp_nonce} ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST}`,
`style-src 'self' 'unsafe-inline' ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST}`,
`img-src data: blob: ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST} *`,
`connect-src 'self' wss: https: ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST}`,
`font-src 'self'`,
`media-src 'self' ${scheme_source} ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST}`,
`worker-src 'self' ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS} ${LOCALHOST} ${scheme_source}`,
`object-src 'none'`,
`form-action 'self'`,
`base-uri 'self'`,
`frame-src 'self' ${SENTRY_DOMAINS} ${GOOGLE_DOMAINS} ${OTHER_DOMAINS}`,
`sandbox allow-scripts allow-same-origin allow-popups allow-forms`,
`upgrade-insecure-requests`
].join('; ');

return [
{
source: '/chat/(.*)',
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'no-referrer' },
{
key: 'Content-Security-Policy',
value: [
`default-src 'self' ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${csp} ${NECESSARY_DOMAINS}`,
`style-src 'self' 'unsafe-inline' ${csp} ${NECESSARY_DOMAINS}`,
`media-src 'self' http: ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
`worker-src 'self' ${csp} ${NECESSARY_DOMAINS} ${scheme_source}`,
`img-src * data: blob:`,
`font-src 'self'`,
`connect-src 'self' wss: https: ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
"object-src 'none'",
"form-action 'self'",
"base-uri 'self'",
"frame-src 'self' 'allow-scripts'",
'sandbox allow-scripts allow-same-origin allow-popups allow-forms',
'upgrade-insecure-requests'
].join('; ')
}
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{ key: 'Content-Security-Policy', value: csp }
]
}
];
},
webpack(config, { isServer, nextRuntime }) {

webpack: (config, { isServer, nextRuntime }) => {
Object.assign(config.resolve.alias, {
'@mongodb-js/zstd': false,
'@aws-sdk/credential-providers': false,
Expand Down
60 changes: 27 additions & 33 deletions projects/app/src/components/Markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import RehypeRaw from 'rehype-raw';
import styles from './index.module.scss';
import dynamic from 'next/dynamic';
import { Box } from '@chakra-ui/react';
import { CodeClassNameEnum, mdTextFormat } from './utils';
import { CodeClassNameEnum, mdTextFormat, filterSafeProps } from './utils';
import ErrorBoundary from './errorBoundry';
import SVGRenderer from './markdowSVG';
import { useCreation } from 'ahooks';
Expand All @@ -37,15 +37,7 @@ const SafeA = (props: any) => {
const href = props.href || '';
const safeHref = isSafeHref(href) ? href : '#';

const ALLOWED_A_ATTRS = new Set([
'href',
'target',
'rel',
'className',
'children',
'style',
'title'
]);
const ALLOWED_A_ATTRS = new Set(['href']);
const safeProps = filterSafeProps(props, ALLOWED_A_ATTRS);

return (
Expand Down Expand Up @@ -130,30 +122,27 @@ const MarkdownRender = ({
node.type = 'text';
node.value = `<${node.tagName}`;
}

// handle properties, filter events
// use filterSafeProps to filter component properties
if (node.properties) {
Object.keys(node.properties).forEach((key) => {
const keyLower = key.toLowerCase();
// if event property (on开头)
if (keyLower.startsWith('on')) {
const value = node.properties[key];
// if event value is not a function or contains suspicious content, delete the event
if (
typeof value === 'string' || // delete event handler in string format
value === null ||
value === undefined ||
(typeof value === 'string' &&
(value.includes('javascript:') ||
value.includes('alert') ||
value.includes('eval') ||
value.includes('Function') ||
/[\(\)\[\]\{\}]/.test(value))) // flag for executable code containing parentheses, etc.
) {
delete node.properties[key];
}
}
});
const ALLOWED_ATTRS = new Set([
'title',
'alt',
'src',
'href',
'target',
'rel',
'width',
'height',
'align',
'valign',
'type',
'lang',
'value',
'name'
]);

// use filterSafeProps to filter properties
node.properties = filterSafeProps(node.properties, ALLOWED_ATTRS);
}
}

Expand Down Expand Up @@ -254,6 +243,11 @@ function sanitizeImageSrc(src?: string): string | undefined {
return undefined;
}

function Image({ src }: { src?: string }) {
const safeSrc = sanitizeImageSrc(src);
return <MdImage src={safeSrc} />;
}

const ALLOWED_IMG_ATTRS = new Set([
'alt',
'width',
Expand Down
76 changes: 13 additions & 63 deletions projects/app/src/components/Markdown/markdowSVG.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import ErrorBoundary from './errorBoundry';
import { filterSafeProps } from './index';
import { filterSafeProps } from './utils';

interface SVGProps {
children?: React.ReactNode;
Expand All @@ -26,80 +26,30 @@ const SVG_ALLOWED_ATTRS = new Set([
]);

const SVGRenderer = ({ children, className, style, ...props }: SVGProps) => {
// filter props
const svgProps = { ...props, className, style };
const sanitizedProps = filterSafeProps(svgProps, SVG_ALLOWED_ATTRS, false);
const sanitizedProps = filterSafeProps({ ...props, className, style }, SVG_ALLOWED_ATTRS);

const sanitizeSVGContent = (content: string | React.ReactNode): string => {
if (typeof content !== 'string') {
return '';
}
if (typeof content !== 'string') return '';

let cleaned = content;

cleaned = cleaned.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
cleaned = cleaned.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
cleaned = cleaned.replace(
/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject>/gi,
''
);

cleaned = cleaned.replace(/\son\w+="[^"]*"/gi, '');
cleaned = cleaned.replace(/\son\w+='[^']*'/gi, '');
cleaned = cleaned.replace(/url\s*\(\s*['"]?\s*javascript:[^)]+\)/gi, '');
cleaned = cleaned.replace(/\bhref="javascript:[^"]*"/gi, '');
cleaned = cleaned.replace(/\bhref='javascript:[^']*'/gi, '');
cleaned = cleaned.replace(/\bxlink:href="javascript:[^"]*"/gi, '');
cleaned = cleaned.replace(/\bxlink:href='javascript:[^']*'/gi, '');
cleaned = cleaned.replace(/\bxmlns(:xlink)?=['"]?javascript:[^"']*['"]?/gi, '');
cleaned = cleaned.replace(/style\s*=\s*(['"])(?:(?!\1).)*javascript:.*?\1/gi, '');

cleaned = cleaned.replace(/\bdata:[^,]*?;base64,[^"')]*["')]/gi, (match) => {
return match.toLowerCase().includes('javascript') ? '' : match;
});

const ALLOWED_ATTRS = new Set([
'width',
'height',
'viewBox',
'fill',
'stroke',
'd',
'x',
'y',
'cx',
'cy',
'r',
'class',
'style'
]);
cleaned = cleaned.replace(/\s(\w+)=['"][^'"]*['"]/gi, (match, attr) => {
return ALLOWED_ATTRS.has(attr.toLowerCase()) ? match : '';
});

cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, '');

return cleaned;
return content
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject>/gi, '')
.replace(/<!--[\s\S]*?-->/g, '');
};

const sanitizedContent = React.Children.map(children, (child) => {
if (typeof child === 'string') {
return sanitizeSVGContent(child);
}
return child;
});

return (
<ErrorBoundary fallback={<div>Something went wrong while rendering Markdown.</div>}>
<ErrorBoundary fallback={<div>SVG rendering error</div>}>
<svg
{...sanitizedProps}
className={className}
style={style}
dangerouslySetInnerHTML={
typeof children === 'string' ? { __html: sanitizeSVGContent(children) } : undefined
}
>
{typeof children !== 'string' && sanitizedContent}
{typeof children !== 'string' &&
React.Children.map(children, (child) =>
typeof child === 'string' ? sanitizeSVGContent(child) : child
)}
</svg>
</ErrorBoundary>
);
Expand Down
Loading
Loading