Răsfoiți Sursa

Merge pull request #8836 from weseek/fix/141984-146887-video-width

fix: Match width of video tag to img tag
Yuki Takei 1 an în urmă
părinte
comite
8a9c3e5821
41 a modificat fișierele cu 349 adăugiri și 541 ștergeri
  1. 6 6
      apps/app/src/client/services/renderer/renderer.tsx
  2. 0 96
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx
  3. 79 0
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.tsx
  4. 10 10
      apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  5. 4 6
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  6. 3 7
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx
  7. 0 6
      apps/app/src/interfaces/rehype.ts
  8. 15 0
      apps/app/src/interfaces/services/rehype-sanitize.ts
  9. 2 2
      apps/app/src/interfaces/services/renderer.ts
  10. 3 3
      apps/app/src/pages/[[...path]].page.tsx
  11. 3 3
      apps/app/src/pages/_private-legacy-pages.page.tsx
  12. 3 3
      apps/app/src/pages/_search.page.tsx
  13. 3 3
      apps/app/src/pages/me/[[...path]].page.tsx
  14. 4 4
      apps/app/src/pages/share/[[...path]].page.tsx
  15. 1 15
      apps/app/src/server/crowi/index.js
  16. 3 2
      apps/app/src/server/models/config.ts
  17. 3 20
      apps/app/src/server/routes/apiv3/page/update-page.ts
  18. 3 2
      apps/app/src/server/routes/apiv3/user-group.js
  19. 0 13
      apps/app/src/server/routes/page.js
  20. 1 28
      apps/app/src/server/service/customize.ts
  21. 8 7
      apps/app/src/server/service/page/index.ts
  22. 2 1
      apps/app/src/server/service/slack-command-handler/create-page-service.js
  23. 0 73
      apps/app/src/server/service/xss.js
  24. 39 0
      apps/app/src/services/general-xss-filter/general-xss-filter.spec.ts
  25. 37 0
      apps/app/src/services/general-xss-filter/general-xss-filter.ts
  26. 1 0
      apps/app/src/services/general-xss-filter/index.ts
  27. 38 0
      apps/app/src/services/renderer/recommended-whitelist.spec.ts
  28. 29 0
      apps/app/src/services/renderer/recommended-whitelist.ts
  29. 12 23
      apps/app/src/services/renderer/renderer.tsx
  30. 0 42
      apps/app/src/services/xss/commonmark-spec.js
  31. 0 63
      apps/app/src/services/xss/index.js
  32. 0 21
      apps/app/src/services/xss/recommended-whitelist.js
  33. 0 32
      apps/app/src/services/xss/xssOption.ts
  34. 0 10
      apps/app/src/stores/xss.ts
  35. 1 1
      apps/app/src/styles/organisms/_wiki.scss
  36. 0 3
      apps/app/test/integration/service/page-grant.test.ts
  37. 9 8
      apps/app/test/integration/service/page.test.js
  38. 10 9
      apps/app/test/integration/service/v5.non-public-page.test.ts
  39. 0 2
      apps/app/test/integration/service/v5.page.test.ts
  40. 17 16
      apps/app/test/integration/service/v5.public-page.test.ts
  41. 0 1
      apps/app/test/integration/setup-crowi.ts

+ 6 - 6
apps/app/src/client/services/renderer/renderer.tsx

@@ -3,7 +3,6 @@ import assert from 'assert';
 import { isClient } from '@growi/core/dist/utils/browser-utils';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
-// eslint-disable-next-line import/extensions
 import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
 import katex from 'rehype-katex';
 import sanitize from 'rehype-sanitize';
@@ -20,8 +19,8 @@ import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
@@ -36,6 +35,7 @@ import loggerFactory from '~/utils/logger';
 
 // import EasyGrid from './PreProcessor/EasyGrid';
 
+
 import '@growi/remark-lsx/dist/client/style.css';
 import '@growi/remark-attachment-refs/dist/client/style.css';
 
@@ -71,7 +71,7 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -132,7 +132,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   // add remark plugins
   // remarkPlugins.push();
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -184,7 +184,7 @@ export const generateSimpleViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 
@@ -277,7 +277,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 

+ 0 - 96
apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx

@@ -1,96 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
-
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class WhitelistInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.tagWhitelist = React.createRef();
-    this.attrWhitelist = React.createRef();
-
-    this.tags = sanitizeDefaultSchema.tagNames;
-    this.attrs = JSON.stringify(sanitizeDefaultSchema.attributes);
-
-    this.onClickRecommendTagButton = this.onClickRecommendTagButton.bind(this);
-    this.onClickRecommendAttrButton = this.onClickRecommendAttrButton.bind(this);
-  }
-
-  onClickRecommendTagButton() {
-    this.tagWhitelist.current.value = this.tags;
-    this.props.adminMarkDownContainer.setState({ tagWhitelist: this.tags });
-  }
-
-  onClickRecommendAttrButton() {
-    this.attrWhitelist.current.value = this.attrs;
-    this.props.adminMarkDownContainer.setState({ attrWhitelist: this.attrs });
-  }
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-
-    return (
-      <>
-        <div className="mt-4">
-          <div className="d-flex justify-content-between">
-            {t('markdown_settings.xss_options.tag_names')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendTagButton}>
-              {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedTags"
-            rows="6"
-            cols="40"
-            ref={this.tagWhitelist}
-            defaultValue={adminMarkDownContainer.state.tagWhitelist}
-            onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
-          />
-        </div>
-        <div className="mt-4">
-          <div className="d-flex justify-content-between">
-            {t('markdown_settings.xss_options.tag_attributes')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={this.onClickRecommendAttrButton}>
-              {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
-            </p>
-          </div>
-          <textarea
-            className="form-control xss-list"
-            name="recommendedAttrs"
-            rows="6"
-            cols="40"
-            ref={this.attrWhitelist}
-            defaultValue={adminMarkDownContainer.state.attrWhitelist}
-            onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
-          />
-        </div>
-      </>
-    );
-  }
-
-}
-
-
-WhitelistInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-const PresentationFormWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <WhitelistInput t={t} {...props} />;
-};
-
-const WhitelistWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
-
-export default WhitelistWrapper;

+ 79 - 0
apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.tsx

@@ -0,0 +1,79 @@
+import { useCallback, useRef } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+
+type Props ={
+  adminMarkDownContainer: AdminMarkDownContainer
+}
+
+export const WhitelistInput = (props: Props): JSX.Element => {
+
+  const { t } = useTranslation('admin');
+  const { adminMarkDownContainer } = props;
+
+  const tagNamesRef = useRef<HTMLTextAreaElement>(null);
+  const attrsRef = useRef<HTMLTextAreaElement>(null);
+
+  const clickRecommendTagButtonHandler = useCallback(() => {
+    if (tagNamesRef.current == null) {
+      return;
+    }
+
+    const tagWhitelist = recommendedTagNames.join(',');
+    tagNamesRef.current.value = tagWhitelist;
+    adminMarkDownContainer.setState({ tagWhitelist });
+  }, [adminMarkDownContainer]);
+
+  const clickRecommendAttrButtonHandler = useCallback(() => {
+    if (attrsRef.current == null) {
+      return;
+    }
+
+    const attrWhitelist = JSON.stringify(recommendedAttributes);
+    attrsRef.current.value = attrWhitelist;
+    adminMarkDownContainer.setState({ attrWhitelist });
+  }, [adminMarkDownContainer]);
+
+  return (
+    <>
+      <div className="mt-4">
+        <div className="d-flex justify-content-between">
+          {t('markdown_settings.xss_options.tag_names')}
+          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendTagButtonHandler}>
+            {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
+          </p>
+        </div>
+        <textarea
+          ref={tagNamesRef}
+          className="form-control xss-list"
+          name="recommendedTags"
+          rows={6}
+          cols={40}
+          defaultValue={adminMarkDownContainer.state.tagWhitelist}
+          onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
+        />
+      </div>
+      <div className="mt-4">
+        <div className="d-flex justify-content-between">
+          {t('markdown_settings.xss_options.tag_attributes')}
+          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendAttrButtonHandler}>
+            {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
+          </p>
+        </div>
+        <textarea
+          ref={attrsRef}
+          className="form-control xss-list"
+          name="recommendedAttrs"
+          rows={6}
+          cols={40}
+          defaultValue={adminMarkDownContainer.state.attrWhitelist}
+          onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
+        />
+      </div>
+    </>
+  );
+
+};

+ 10 - 10
apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -2,17 +2,17 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-import WhitelistInput from './WhitelistInput';
+import { WhitelistInput } from './WhitelistInput';
 
 const logger = loggerFactory('growi:importer');
 
@@ -41,8 +41,8 @@ class XssForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { xssOption } = adminMarkDownContainer.state;
 
-    const rehypeRecommendedTags = sanitizeDefaultSchema.tagNames;
-    const rehypeRecommendedAttributes = JSON.stringify(sanitizeDefaultSchema.attributes);
+    const rehypeRecommendedTags = recommendedTagNames.join(',');
+    const rehypeRecommendedAttributes = JSON.stringify(recommendedAttributes);
 
     return (
       <div className="col-12 mt-3">
@@ -55,8 +55,8 @@ class XssForm extends React.Component {
                 className="form-check-input"
                 id="xssOption1"
                 name="XssOption"
-                checked={xssOption === RehypeSanitizeOption.RECOMMENDED}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.RECOMMENDED }) }}
+                checked={xssOption === RehypeSanitizeType.RECOMMENDED}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.RECOMMENDED }) }}
               />
               <label className="form-label form-check-label w-100" htmlFor="xssOption1">
                 <p className="fw-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
@@ -97,12 +97,12 @@ class XssForm extends React.Component {
                 className="form-check-input"
                 id="xssOption2"
                 name="XssOption"
-                checked={xssOption === RehypeSanitizeOption.CUSTOM}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
+                checked={xssOption === RehypeSanitizeType.CUSTOM}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.CUSTOM }) }}
               />
               <label className="form-label form-check-label w-100" htmlFor="xssOption2">
                 <p className="fw-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
-                <WhitelistInput customizable />
+                <WhitelistInput adminMarkDownContainer={adminMarkDownContainer} />
               </label>
             </div>
           </div>

+ 4 - 6
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useState, useCallback, useEffect, useMemo,
+  useState, useCallback, useEffect,
 } from 'react';
 
 import {
@@ -18,7 +18,6 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import type { PageActionOnGroupDelete, SearchType } from '~/interfaces/user-group';
 import { SearchTypes } from '~/interfaces/user-group';
-import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import { useSWRxUserGroupPages, useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups } from '~/stores/user-group';
@@ -54,7 +53,6 @@ type Props = {
 const UserGroupDetailPage = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const router = useRouter();
-  const xss = useMemo(() => new Xss(), []);
   const { userGroupId: currentUserGroupId, isExternalGroup } = props;
 
   const { data: currentUserGroup } = useUserGroup(currentUserGroupId, isExternalGroup);
@@ -221,13 +219,13 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
   const removeUserByUsername = useCallback(async(username: string) => {
     try {
       await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
-      toastSuccess(`Removed "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`);
+      toastSuccess(`Removed "${username}" from "${currentUserGroup?.name}"`);
       mutateUserGroupRelationList();
     }
     catch (err) {
-      toastError(new Error(`Unable to remove "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`));
+      toastError(new Error(`Unable to remove "${username}" from "${currentUserGroup?.name}"`));
     }
-  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList, xss]);
+  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList]);
 
   const showUpdateModal = useCallback((group: IUserGroupHasId) => {
     setUpdateModalShown(true);

+ 3 - 7
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx

@@ -1,5 +1,5 @@
 import type { FC, KeyboardEvent } from 'react';
-import React, { useState, useRef } from 'react';
+import React, { useState } from 'react';
 
 import type { IUserGroupHasId, IUserHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -8,7 +8,6 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { SearchType } from '~/interfaces/user-group';
-import Xss from '~/services/xss';
 
 type Props = {
   userGroup: IUserGroupHasId,
@@ -25,25 +24,22 @@ export const UserGroupUserFormByInput: FC<Props> = (props) => {
   } = props;
 
   const { t } = useTranslation();
-  const typeaheadRef = useRef(null);
   const [inputUser, setInputUser] = useState<IUserHasId[]>([]);
   const [applicableUsers, setApplicableUsers] = useState<IUserHasId[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [isSearchError, setIsSearchError] = useState(false);
 
-  const xss = new Xss();
-
   const addUserBySubmit = async() => {
     if (inputUser.length === 0) { return }
     const userName = inputUser[0].username;
 
     try {
       await onClickAddUserBtn(userName);
-      toastSuccess(`Added "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`);
+      toastSuccess(`Added "${userName}" to "${userGroup.name}"`);
       setInputUser([]);
     }
     catch (err) {
-      toastError(new Error(`Unable to add "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`));
+      toastError(new Error(`Unable to add "${userName}" to "${userGroup.name}"`));
     }
   };
 

+ 0 - 6
apps/app/src/interfaces/rehype.ts

@@ -1,6 +0,0 @@
-export const RehypeSanitizeOption = {
-  RECOMMENDED: 'Recommended',
-  CUSTOM: 'Custom',
-} as const;
-
-export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];

+ 15 - 0
apps/app/src/interfaces/services/rehype-sanitize.ts

@@ -0,0 +1,15 @@
+import type { Attributes } from 'hast-util-sanitize/lib';
+
+export const RehypeSanitizeType = {
+  RECOMMENDED: 'Recommended',
+  CUSTOM: 'Custom',
+} as const;
+
+export type RehypeSanitizeType = typeof RehypeSanitizeType[keyof typeof RehypeSanitizeType];
+
+export type RehypeSanitizeConfiguration = {
+  isEnabledXssPrevention: boolean,
+  sanitizeType: RehypeSanitizeType,
+  customTagWhitelist?: Array<string> | null,
+  customAttrWhitelist?: Attributes | null,
+}

+ 2 - 2
apps/app/src/interfaces/services/renderer.ts

@@ -1,4 +1,4 @@
-import { XssOptionConfig } from '~/services/xss/xssOption';
+import type { RehypeSanitizeConfiguration } from './rehype-sanitize';
 
 export type RendererConfig = {
   isSharedPage?: boolean
@@ -11,4 +11,4 @@ export type RendererConfig = {
 
   drawioUri: string,
   plantumlUri: string,
-} & XssOptionConfig;
+} & RehypeSanitizeConfiguration;

+ 3 - 3
apps/app/src/pages/[[...path]].page.tsx

@@ -566,9 +566,9 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 3 - 3
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -114,9 +114,9 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 3 - 3
apps/app/src/pages/_search.page.tsx

@@ -141,9 +141,9 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 3 - 3
apps/app/src/pages/me/[[...path]].page.tsx

@@ -196,9 +196,9 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 4 - 4
apps/app/src/pages/share/[[...path]].page.tsx

@@ -173,10 +173,10 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
+    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 
   props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');

+ 1 - 15
apps/app/src/server/crowi/index.js

@@ -14,7 +14,6 @@ import { KeycloakUserGroupSyncService } from '~/features/external-user-group/ser
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
-import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
@@ -81,7 +80,6 @@ class Crowi {
     this.mailService = null;
     this.passportService = null;
     this.globalNotificationService = null;
-    this.xssService = null;
     this.aclService = null;
     this.appService = null;
     this.fileUploadService = null;
@@ -97,7 +95,6 @@ class Crowi {
     this.inAppNotificationService = null;
     this.activityService = null;
     this.commentService = null;
-    this.xss = new Xss();
     this.questionnaireService = null;
     this.questionnaireCronService = null;
 
@@ -133,12 +130,11 @@ Crowi.prototype.init = async function() {
   await this.setupS2sMessagingService();
   await this.setupSocketIoService();
 
-  // customizeService depends on AppService and XssService
+  // customizeService depends on AppService
   // passportService depends on appService
   // export and import depends on setUpGrowiBridge
   await Promise.all([
     this.setUpApp(),
-    this.setUpXss(),
     this.setUpGrowiBridge(),
   ]);
 
@@ -597,16 +593,6 @@ Crowi.prototype.setUpUserNotification = async function() {
   }
 };
 
-/**
- * setup XssService
- */
-Crowi.prototype.setUpXss = async function() {
-  const XssService = require('../service/xss');
-  if (this.xssService == null) {
-    this.xssService = new XssService(this.configManager);
-  }
-};
-
 /**
  * setup AclService
  */

+ 3 - 2
apps/app/src/server/models/config.ts

@@ -3,7 +3,8 @@ import type { Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 
-import { RehypeSanitizeOption } from '../../interfaces/rehype';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
+
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
@@ -161,7 +162,7 @@ export const defaultMarkdownConfigs: { [key: string]: any } = {
   'markdown:xss:attrWhitelist': [],
 
   'markdown:rehypeSanitize:isEnabledPrevention': true,
-  'markdown:rehypeSanitize:option': RehypeSanitizeOption.RECOMMENDED,
+  'markdown:rehypeSanitize:option': RehypeSanitizeType.RECOMMENDED,
   'markdown:rehypeSanitize:tagNames': [],
   'markdown:rehypeSanitize:attributes': '{}',
   'markdown:isEnabledLinebreaks': false,

+ 3 - 20
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -11,18 +11,15 @@ import mongoose from 'mongoose';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import {
   GlobalNotificationSettingEvent, serializePageSecurely, serializeRevisionSecurely, serializeUserSecurely,
 } from '~/server/models';
 import type { PageDocument, PageModel } from '~/server/models/page';
-import { configManager } from '~/server/service/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
-import Xss from '~/services/xss';
-import XssOption from '~/services/xss/xssOption';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
@@ -47,20 +44,6 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
-
-  const xss = (() => {
-    const initializedConfig = {
-      isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-      tagWhitelist: crowi.xssService.getTagWhitelist(),
-      attrWhitelist: crowi.xssService.getAttrWhitelist(),
-      // TODO: Omit rehype related property from XssOptionConfig type
-      //  Server side xss implementation does not require it.
-      xssOption: RehypeSanitizeOption.CUSTOM,
-    };
-    const xssOption = new XssOption(initializedConfig);
-    return new Xss(xssOption);
-  })();
-
   // define validators for req.body
   const validator: ValidationChain[] = [
     body('pageId').exists().not().isEmpty({ ignore_whitespace: true })
@@ -138,7 +121,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         pageId, revisionId, body, origin,
       } = req.body;
 
-      const sanitizeRevisionId = revisionId == null ? undefined : xss.process(revisionId);
+      const sanitizeRevisionId = revisionId == null ? undefined : generalXssFilter.process(revisionId);
 
       // check page existence
       const isExist = await Page.count({ _id: pageId }) > 0;
@@ -153,7 +136,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),
-          revisionBody: xss.process(latestRevision?.body),
+          revisionBody: latestRevision?.body,
           createdAt: latestRevision?.createdAt,
           user: serializeUserSecurely(latestRevision?.author),
         };

+ 3 - 2
apps/app/src/server/routes/apiv3/user-group.js

@@ -7,6 +7,7 @@ import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/
 import UserGroup from '~/server/models/user-group';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -230,8 +231,8 @@ module.exports = (crowi) => {
     const { name, description = '', parentId } = req.body;
 
     try {
-      const userGroupName = crowi.xss.process(name);
-      const userGroupDescription = crowi.xss.process(description);
+      const userGroupName = generalXssFilter.process(name);
+      const userGroupDescription = generalXssFilter.process(description);
       const userGroup = await UserGroup.createGroup(userGroupName, userGroupDescription, parentId);
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_CREATE };

+ 0 - 13
apps/app/src/server/routes/page.js

@@ -1,14 +1,12 @@
 import { body } from 'express-validator';
 import mongoose from 'mongoose';
 
-import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
 
 import { GlobalNotificationSettingEvent } from '../models';
 import { PathAlreadyExistsError } from '../models/errors';
 import PageTagRelation from '../models/page-tag-relation';
 import UpdatePost from '../models/update-post';
-import { configManager } from '../service/config-manager';
 
 /**
  * @swagger
@@ -146,19 +144,8 @@ module.exports = function(crowi, app) {
 
   const ApiResponse = require('../util/apiResponse');
 
-  const { xssService } = crowi;
   const globalNotificationService = crowi.getGlobalNotificationService();
 
-  const Xss = require('~/services/xss/index');
-  const initializedConfig = {
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-    tagWhitelist: xssService.getTagWhitelist(),
-    attrWhitelist: xssService.getAttrWhitelist(),
-  };
-  const xssOption = new XssOption(initializedConfig);
-  const xss = new Xss(xssOption);
-
-
   const actions = {};
 
   // async function showPageForPresentation(req, res, next) {

+ 1 - 28
apps/app/src/server/service/customize.ts

@@ -1,7 +1,6 @@
 import path from 'path';
 
 import type { ColorScheme } from '@growi/core';
-import { DevidedPagePath } from '@growi/core/dist/models';
 import { getForcedColorScheme } from '@growi/core/dist/utils';
 import { DefaultThemeMetadata, PresetThemesMetadatas, manifestPath } from '@growi/preset-themes';
 import uglifycss from 'uglifycss';
@@ -11,6 +10,7 @@ import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
 
+
 import type { ConfigManager } from './config-manager';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
@@ -29,8 +29,6 @@ class CustomizeService implements S2sMessageHandlable {
 
   appService: any;
 
-  xssService: any;
-
   lastLoadedAt?: Date;
 
   customCss?: string;
@@ -47,7 +45,6 @@ class CustomizeService implements S2sMessageHandlable {
     this.configManager = crowi.configManager;
     this.s2sMessagingService = crowi.s2sMessagingService;
     this.appService = crowi.appService;
-    this.xssService = crowi.xssService;
   }
 
   /**
@@ -126,30 +123,6 @@ class CustomizeService implements S2sMessageHandlable {
     this.lastLoadedAt = new Date();
   }
 
-  generateCustomTitle(pageOrPath) {
-    const path = pageOrPath.path || pageOrPath;
-    const dPagePath = new DevidedPagePath(path, true, true);
-
-    const customTitle = this.customTitleTemplate
-      .replace('{{sitename}}', this.appService.getAppTitle())
-      .replace('{{pagepath}}', path)
-      .replace('{{page}}', dPagePath.latter) // for backward compatibility
-      .replace('{{pagename}}', dPagePath.latter);
-
-    return this.xssService.process(customTitle);
-  }
-
-  generateCustomTitleForFixedPageName(title) {
-    // replace
-    const customTitle = this.customTitleTemplate
-      .replace('{{sitename}}', this.appService.getAppTitle())
-      .replace('{{page}}', title)
-      .replace('{{pagepath}}', title)
-      .replace('{{pagename}}', title);
-
-    return this.xssService.process(customTitle);
-  }
-
   async initGrowiTheme(): Promise<void> {
     const theme = this.configManager.getConfig('crowi', 'customize:theme');
 

+ 8 - 7
apps/app/src/server/service/page/index.ts

@@ -44,6 +44,7 @@ import type { UserGroupDocument } from '~/server/models/user-group';
 import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { collectAncestorPaths } from '~/server/util/collect-ancestor-paths';
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
@@ -610,7 +611,7 @@ class PageService implements IPageService {
 
     const updateMetadata = options.updateMetadata || false;
     // sanitize path
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // UserGroup & Owner validation
     // use the parent's grant when target page is an empty page
@@ -839,7 +840,7 @@ class PageService implements IPageService {
     } = options;
 
     // sanitize path
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // create descendants first
     if (isRecursively) {
@@ -1104,7 +1105,7 @@ class PageService implements IPageService {
       throw Error('Page not found.');
     }
 
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // 1. Separate v4 & v5 process
     const isShouldUseV4Process = shouldUseV4Process(page);
@@ -1278,7 +1279,7 @@ class PageService implements IPageService {
     options.grantUserGroupIds = page.grantedGroups;
     options.grantedUserIds = page.grantedUsers;
 
-    newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
+    newPagePath = generalXssFilter.process(newPagePath); // eslint-disable-line no-param-reassign
 
     const createdPage = await this.create(
       newPagePath, page.revision.body, user, options,
@@ -3777,7 +3778,7 @@ class PageService implements IPageService {
     }
 
     // Values
-    const path: string = this.crowi.xss.process(_path); // sanitize path
+    const path: string = generalXssFilter.process(_path); // sanitize path
 
     // Retrieve closest ancestor document
     const Page = mongoose.model<PageDocument, PageModel>('Page');
@@ -3907,7 +3908,7 @@ class PageService implements IPageService {
     const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // sanitize path
-    path = this.crowi.xss.process(path); // eslint-disable-line no-param-reassign
+    path = generalXssFilter.process(path); // eslint-disable-line no-param-reassign
 
     let grant = options.grant;
     // force public
@@ -3988,7 +3989,7 @@ class PageService implements IPageService {
 
     // Values
     // eslint-disable-next-line no-param-reassign
-    path = this.crowi.xss.process(path); // sanitize path
+    path = generalXssFilter.process(path); // sanitize path
 
     const {
       grantUserGroupIds, grantUserIds,

+ 2 - 1
apps/app/src/server/service/slack-command-handler/create-page-service.js

@@ -1,6 +1,7 @@
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import { reshapeContentsBody } from '@growi/slack/dist/utils/reshape-contents-body';
 
+import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
 // eslint-disable-next-line no-unused-vars
@@ -19,7 +20,7 @@ class CreatePageService {
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
 
     // sanitize path
-    const sanitizedPath = this.crowi.xss.process(path);
+    const sanitizedPath = generalXssFilter.process(path);
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
 
     // Since an ObjectId is required for creating a page, if a user does not exist, a dummy user will be generated

+ 0 - 73
apps/app/src/server/service/xss.js

@@ -1,73 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:service:XssSerivce'); // eslint-disable-line no-unused-vars
-
-const Xss = require('~/services/xss');
-const { tags, attrs } = require('~/services/xss/recommended-whitelist');
-
-/**
- * the service class of XssSerivce
- */
-class XssSerivce {
-
-  constructor(configManager) {
-    this.configManager = configManager;
-
-    this.xss = new Xss();
-  }
-
-  process(value) {
-    return this.xss.process(value);
-  }
-
-  getTagWhitelist() {
-    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
-    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
-
-    if (isEnabledXssPrevention) {
-      switch (xssOpiton) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return tags;
-
-        case 3: // custom whitelist
-          return this.configManager.getConfig('markdown', 'markdown:xss:tagWhitelist');
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  }
-
-  getAttrWhitelist() {
-    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
-    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
-
-    if (isEnabledXssPrevention) {
-      switch (xssOpiton) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return attrs;
-
-        case 3: // custom whitelist
-          return this.configManager.getConfig('markdown', 'markdown:xss:attrWhitelist');
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  }
-
-}
-
-module.exports = XssSerivce;

+ 39 - 0
apps/app/src/services/general-xss-filter/general-xss-filter.spec.ts

@@ -0,0 +1,39 @@
+import { generalXssFilter } from './general-xss-filter';
+
+describe('generalXssFilter', () => {
+
+  test('should be sanitize script tag', () => {
+    // Act
+    const result = generalXssFilter.process('<script>alert("XSS")</script>');
+
+    // Assert
+    expect(result).toBe('alert("XSS")');
+  });
+
+  test('should be sanitize nested script tag recursively', () => {
+    // Act
+    const result = generalXssFilter.process('<scr<script>ipt>alert("XSS")</scr<script>ipt>');
+
+    // Assert
+    expect(result).toBe('alert("XSS")');
+  });
+
+  // for https://github.com/weseek/growi/issues/221
+  test('should not be sanitize blockquote', () => {
+    // Act
+    const result = generalXssFilter.process('> foo\n> bar');
+
+    // Assert
+    expect(result).toBe('> foo\n> bar');
+  });
+
+  // https://github.com/weseek/growi/pull/505
+  test('should not be sanitize next closing-tag', () => {
+    // Act
+    const result = generalXssFilter.process('<evil /><span>text</span>');
+
+    // Assert
+    expect(result).toBe('<span>text</span>');
+  });
+
+});

+ 37 - 0
apps/app/src/services/general-xss-filter/general-xss-filter.ts

@@ -0,0 +1,37 @@
+import type { IFilterXSSOptions } from 'xss';
+import { FilterXSS } from 'xss';
+
+const REPETITIONS_NUM = 50;
+
+const option: IFilterXSSOptions = {
+  stripIgnoreTag: true,
+  stripIgnoreTagBody: false, // see https://github.com/weseek/growi/pull/505
+  css: false,
+  escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
+};
+
+class GeneralXssFilter extends FilterXSS {
+
+  override process(document: string | undefined): string {
+    let count = 0;
+    let currDoc = document;
+    let prevDoc = document;
+
+    do {
+      count += 1;
+      // stop running infinitely
+      if (count > REPETITIONS_NUM) {
+        return '--filtered--';
+      }
+
+      prevDoc = currDoc;
+      currDoc = super.process(currDoc ?? '');
+    }
+    while (currDoc !== prevDoc);
+
+    return currDoc;
+  }
+
+}
+
+export const generalXssFilter = new GeneralXssFilter(option);

+ 1 - 0
apps/app/src/services/general-xss-filter/index.ts

@@ -0,0 +1 @@
+export * from './general-xss-filter';

+ 38 - 0
apps/app/src/services/renderer/recommended-whitelist.spec.ts

@@ -0,0 +1,38 @@
+import { tagNames, attributes } from './recommended-whitelist';
+
+describe('recommended-whitelist', () => {
+
+  test('.tagNames should return iframe tag', () => {
+    expect(tagNames).not.toBeNull();
+    expect(tagNames).includes('iframe');
+  });
+
+  test('.tagNames should return video tag', () => {
+    expect(tagNames).not.toBeNull();
+    expect(tagNames).includes('video');
+  });
+
+  test('.attributes should return data attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('*');
+    expect(attributes['*']).includes('alt');
+    expect(attributes['*']).includes('align');
+    expect(attributes['*']).includes('width');
+    expect(attributes['*']).includes('height');
+    expect(attributes['*']).includes('className');
+    expect(attributes['*']).includes('data*');
+  });
+
+  test('.attributes should return iframe attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('iframe');
+    expect(attributes.iframe).includes('src');
+  });
+
+  test('.attributes should return video attributes', () => {
+    expect(attributes).not.toBeNull();
+    expect(Object.keys(attributes)).includes('video');
+    expect(attributes.iframe).includes('src');
+  });
+
+});

+ 29 - 0
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -0,0 +1,29 @@
+import { defaultSchema } from 'hast-util-sanitize';
+import type { Attributes } from 'hast-util-sanitize/lib';
+import deepmerge from 'ts-deepmerge';
+
+/**
+ * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
+ *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41
+ */
+
+export const tagNames: Array<string> = [
+  ...defaultSchema.tagNames ?? [],
+  '-', 'bdi',
+  'col', 'colgroup',
+  'data',
+  'iframe',
+  'video',
+  'rb', 'u',
+];
+
+export const attributes: Attributes = deepmerge(
+  defaultSchema.attributes ?? {},
+  {
+    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
+    video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
+    // The special value 'data*' as a property name can be used to allow all data properties.
+    // see: https://github.com/syntax-tree/hast-util-sanitize/
+    '*': ['key', 'class', 'className', 'style', 'data*'],
+  },
+);

+ 12 - 23
apps/app/src/services/renderer/renderer.tsx

@@ -2,7 +2,7 @@ import growiDirective from '@growi/remark-growi-directive';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
-import sanitize, { defaultSchema as rehypeSanitizeDefaultSchema } from 'rehype-sanitize';
+import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
@@ -16,17 +16,19 @@ import type { Pluggable, PluginTuple } from 'unified';
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 
+import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from './recommended-whitelist';
 import * as addClass from './rehype-plugins/add-class';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 
+
 // import EasyGrid from './PreProcessor/EasyGrid';
 
 
@@ -36,31 +38,18 @@ const logger = loggerFactory('growi:services:renderer');
 
 type SanitizePlugin = PluginTuple<[SanitizeOption]>;
 
-const baseSanitizeSchema = {
-  tagNames: ['iframe', 'section', 'video'],
-  attributes: {
-    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
-    video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
-    // The special value 'data*' as a property name can be used to allow all data properties.
-    // see: https://github.com/syntax-tree/hast-util-sanitize/
-    '*': ['key', 'class', 'className', 'style', 'data*'],
-  },
+export const commonSanitizeOption: SanitizeOption = {
+  tagNames: recommendedTagNames,
+  attributes: recommendedAttributes,
+  clobberPrefix: '', // remove clobber prefix
 };
 
-export const commonSanitizeOption: SanitizeOption = deepmerge(
-  rehypeSanitizeDefaultSchema,
-  baseSanitizeSchema,
-  {
-    clobberPrefix: '', // remove clobber prefix
-  },
-);
-
 let isInjectedCustomSanitaizeOption = false;
 
 export const injectCustomSanitizeOption = (config: RendererConfig): void => {
-  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhitelist ?? []);
-    commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhitelist ?? {});
+  if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.sanitizeType === RehypeSanitizeType.CUSTOM) {
+    commonSanitizeOption.tagNames = config.customTagWhitelist ?? recommendedTagNames;
+    commonSanitizeOption.attributes = config.customAttrWhitelist ?? recommendedAttributes;
     isInjectedCustomSanitaizeOption = true;
   }
 };
@@ -142,7 +131,7 @@ export const generateSSRViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+  if (config.sanitizeType === RehypeSanitizeType.CUSTOM) {
     injectCustomSanitizeOption(config);
   }
 

+ 0 - 42
apps/app/src/services/xss/commonmark-spec.js

@@ -1,42 +0,0 @@
-/**
- * Valid schemes
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-const schemesForAutolink = [
-  'coap', 'doi', 'javascript', 'aaa', 'aaas', 'about', 'acap', 'cap', 'cid', 'crid', 'data', 'dav', 'dict', 'dns',
-  'file', 'ftp', 'geo', 'go', 'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap', 'info', 'ipp', 'iris',
-  'iris.beep', 'iris.xpc', 'iris.xpcs', 'iris.lwz', 'ldap', 'mailto', 'mid', 'msrp', 'msrps', 'mtqp', 'mupdate',
-  'news', 'nfs', 'ni', 'nih', 'nntp', 'opaquelocktoken', 'pop', 'pres', 'rtsp', 'service', 'session', 'shttp',
-  'sieve', 'sip', 'sips', 'sms', 'snmp,soap.beep', 'soap.beeps', 'tag', 'tel', 'telnet', 'tftp', 'thismessage',
-  'tn3270', 'tip', 'tv', 'urn', 'vemmi', 'ws', 'wss', 'xcon', 'xcon-userid', 'xmlrpc.beep', 'xmlrpc.beeps', 'xmpp',
-  'z39.50r', 'z39.50s', 'adiumxtra', 'afp', 'afs', 'aim', 'apt,attachment', 'aw', 'beshare', 'bitcoin', 'bolo',
-  'callto', 'chrome,chrome-extension', 'com-eventbrite-attendee', 'content', 'cvs,dlna-playsingle', 'dlna-playcontainer',
-  'dtn', 'dvb', 'ed2k', 'facetime', 'feed', 'finger', 'fish', 'gg', 'git', 'gizmoproject', 'gtalk', 'hcp', 'icon',
-  'ipn', 'irc', 'irc6', 'ircs', 'itms', 'jar', 'jms', 'keyparc', 'lastfm', 'ldaps', 'magnet', 'maps', 'market,message',
-  'mms', 'ms-help', 'msnim', 'mumble', 'mvn', 'notes', 'oid', 'palm', 'paparazzi', 'platform', 'proxy', 'psyc',
-  'query', 'res', 'resource', 'rmi', 'rsync', 'rtmp', 'secondlife', 'sftp', 'sgn', 'skype', 'smb', 'soldat', 'spotify',
-  'ssh', 'steam', 'svn', 'teamspeak', 'things', 'udp', 'unreal', 'ut2004', 'ventrilo', 'view-source', 'webcal',
-  'wtai', 'wyciwyg', 'xfire', 'xri', 'ymsgr',
-];
-const schemesCondition = schemesForAutolink.join('|');
-
-/**
- * RegExp for URI
- * @type {RegExp}
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-const uriAutolinkRegexp = new RegExp(`^(${schemesCondition}):\\/\\/.+$`);
-
-/**
- * RegExp for email
- * @type {RegExp}
- * @see https://spec.commonmark.org/0.16/#autolinks
- */
-// eslint-disable-next-line max-len
-const emailAutolinkRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-
-
-module.exports = {
-  uriAutolinkRegexp,
-  emailAutolinkRegexp,
-};

+ 0 - 63
apps/app/src/services/xss/index.js

@@ -1,63 +0,0 @@
-const xss = require('xss');
-const commonmarkSpec = require('./commonmark-spec');
-
-
-const REPETITIONS_NUM = 50;
-
-class Xss {
-
-  constructor(xssOption) {
-
-    xssOption = xssOption || {}; // eslint-disable-line no-param-reassign
-
-    const tagWhitelist = xssOption.tagWhitelist || [];
-    const attrWhitelist = xssOption.attrWhitelist || [];
-
-    const whitelistContent = {};
-
-    // default
-    const option = {
-      stripIgnoreTag: true,
-      stripIgnoreTagBody: false, // see https://github.com/weseek/growi/pull/505
-      css: false,
-      whitelist: whitelistContent,
-      escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
-      onTag: (tag, html, options) => {
-        // pass autolink
-        if (tag.match(commonmarkSpec.uriAutolinkRegexp) || tag.match(commonmarkSpec.emailAutolinkRegexp)) {
-          return html;
-        }
-      },
-    };
-
-    tagWhitelist.forEach((tag) => {
-      whitelistContent[tag] = attrWhitelist;
-    });
-
-    // create the XSS Filter instance
-    this.myxss = new xss.FilterXSS(option);
-  }
-
-  process(document) {
-    let count = 0;
-    let currDoc = document;
-    let prevDoc = document;
-
-    do {
-      count += 1;
-      // stop running infinitely
-      if (count > REPETITIONS_NUM) {
-        return '--filtered--';
-      }
-
-      prevDoc = currDoc;
-      currDoc = this.myxss.process(currDoc);
-    }
-    while (currDoc !== prevDoc);
-
-    return currDoc;
-  }
-
-}
-
-module.exports = Xss;

+ 0 - 21
apps/app/src/services/xss/recommended-whitelist.js

@@ -1,21 +0,0 @@
-/**
- * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
- *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41
- */
-
-const tags = [
-  '-', 'a', 'abbr', 'b', 'bdi', 'bdo', 'blockquote', 'br', 'caption', 'cite',
-  'code', 'col', 'colgroup', 'data', 'dd', 'del', 'details', 'dfn', 'div', 'dl',
-  'dt', 'em', 'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7',
-  'h8', 'hr', 'i', 'iframe', 'img', 'ins', 'kbd', 'li', 'mark', 'ol', 'p',
-  'pre', 'q', 'rb', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strike',
-  'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
-  'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr',
-];
-
-const attrs = ['src', 'href', 'class', 'id', 'width', 'height', 'alt', 'title', 'style'];
-
-module.exports = {
-  tags,
-  attrs,
-};

+ 0 - 32
apps/app/src/services/xss/xssOption.ts

@@ -1,32 +0,0 @@
-import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
-
-import type { RehypeSanitizeOption } from '~/interfaces/rehype';
-
-type tagWhitelist = typeof sanitizeDefaultSchema.tagNames;
-type attrWhitelist = typeof sanitizeDefaultSchema.attributes;
-
-export type XssOptionConfig = {
-  isEnabledXssPrevention: boolean,
-  xssOption: RehypeSanitizeOption,
-  tagWhitelist: tagWhitelist,
-  attrWhitelist: attrWhitelist,
-}
-
-export default class XssOption {
-
-  isEnabledXssPrevention: boolean;
-
-  tagWhitelist: any[];
-
-  attrWhitelist: any[];
-
-  constructor(config: XssOptionConfig) {
-    const recommendedWhitelist = require('~/services/xss/recommended-whitelist');
-    const initializedConfig: Partial<XssOptionConfig> = (config != null) ? config : {};
-
-    this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
-    this.tagWhitelist = initializedConfig.tagWhitelist || recommendedWhitelist.tags;
-    this.attrWhitelist = initializedConfig.attrWhitelist || recommendedWhitelist.attrs;
-  }
-
-}

+ 0 - 10
apps/app/src/stores/xss.ts

@@ -1,10 +0,0 @@
-
-import { SWRResponse } from 'swr';
-
-import Xss from '~/services/xss';
-
-import { useStaticSWR } from './use-static-swr';
-
-export const useXss = (initialData?: Xss): SWRResponse<Xss, Error> => {
-  return useStaticSWR<Xss, Error>('xss', initialData);
-};

+ 1 - 1
apps/app/src/styles/organisms/_wiki.scss

@@ -130,7 +130,7 @@
     border-left: 0.3rem solid #ddd;
   }
 
-  img {
+  img,video {
     max-width: 100%;
     margin: 5px 0;
   }

+ 0 - 3
apps/app/test/integration/service/page-grant.test.ts

@@ -26,7 +26,6 @@ describe('PageGrantService', () => {
    */
   let crowi;
   let pageGrantService: IPageGrantService;
-  let xssSpy;
 
   let user1;
   let user2;
@@ -489,8 +488,6 @@ describe('PageGrantService', () => {
 
     await createDocumentsToTestIsGrantNormalized();
     await createDocumentsToTestGetPageGroupGrantData();
-
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
   });
 
   describe('Test isGrantNormalized method with shouldCheckDescendants false', () => {

+ 9 - 8
apps/app/test/integration/service/page.test.js

@@ -8,6 +8,7 @@ import Tag from '~/server/models/tag';
 import UserGroup from '~/server/models/user-group';
 import UserGroupRelation from '~/server/models/user-group-relation';
 
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 
 const mongoose = require('mongoose');
 
@@ -66,7 +67,7 @@ describe('PageService', () => {
   let Bookmark;
   let Comment;
   let ShareLink;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   beforeAll(async() => {
     crowi = await getInstance();
@@ -346,7 +347,7 @@ describe('PageService', () => {
       },
     ]);
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     /**
      * getParentAndFillAncestors
@@ -494,7 +495,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename1,
           '/renamed1', testUser2, {}, { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
 
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -508,7 +509,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename2, '/renamed2', testUser2, { updateMetadata: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
 
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -522,7 +523,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename3, '/renamed3', testUser2, { createRedirectPage: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
         expect(resultPage.path).toBe('/renamed3');
@@ -535,7 +536,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename4, '/renamed4', testUser2, { isRecursively: true },
           { ip: '::ffff:127.0.0.1', endpoint: '/_api/v3/pages/rename' });
 
-        expect(xssSpy).toHaveBeenCalled();
+        expect(generalXssFilterProcessSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).toHaveBeenCalled();
         expect(pageEventSpy).toHaveBeenCalledWith('rename');
 
@@ -625,7 +626,7 @@ describe('PageService', () => {
       const resultPage = await crowi.pageService.duplicate(parentForDuplicate, '/newParentDuplicate', testUser2, false);
       const duplicatedToPageRevision = await Revision.findOne({ pageId: resultPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicateDescendantsWithStreamSpy).not.toHaveBeenCalled();
       // TODO https://redmine.weseek.co.jp/issues/87537 : activate outer module mockImplementation
       // expect(serializePageSecurely).toHaveBeenCalled();
@@ -646,7 +647,7 @@ describe('PageService', () => {
       const resultPageRecursivly = await crowi.pageService.duplicate(parentForDuplicate, '/newParentDuplicateRecursively', testUser2, true);
       const duplicatedRecursivelyToPageRevision = await Revision.findOne({ pageId: resultPageRecursivly._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicateDescendantsWithStreamSpy).toHaveBeenCalled();
       // TODO https://redmine.weseek.co.jp/issues/87537 : activate outer module mockImplementation
       // expect(serializePageSecurely).toHaveBeenCalled();

+ 10 - 9
apps/app/test/integration/service/v5.non-public-page.test.ts

@@ -10,6 +10,7 @@ import PageTagRelation from '../../../src/server/models/page-tag-relation';
 import Tag from '../../../src/server/models/tag';
 import UserGroup from '../../../src/server/models/user-group';
 import UserGroupRelation from '../../../src/server/models/user-group-relation';
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with non-public pages', () => {
@@ -31,7 +32,7 @@ describe('PageService page operations with non-public pages', () => {
   let Page;
   let Revision;
   let User;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   let rootPage;
 
@@ -290,7 +291,7 @@ describe('PageService page operations with non-public pages', () => {
       },
     ]);
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser2 = await User.findOne({ username: 'v5DummyUser2' });
@@ -1139,7 +1140,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(page3Renamed.parent).toStrictEqual(page2Renamed._id);
       expect(normalizeGrantedGroups(page2Renamed.grantedGroups)).toStrictEqual(normalizeGrantedGroups(_page2.grantedGroups));
       expect(normalizeGrantedGroups(page3Renamed.grantedGroups)).toStrictEqual(normalizeGrantedGroups(_page3.grantedGroups));
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
     });
     test('Should throw with NOT grant normalized pages', async() => {
       const _pathD = '/np_rename4_destination';
@@ -1206,7 +1207,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(page2Renamed).toBeTruthy();
       expect(page3Renamed).toBeNull();
       expect(page2Renamed.parent).toBeNull();
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
     });
   });
   describe('Duplicate', () => {
@@ -1240,7 +1241,7 @@ describe('PageService page operations with non-public pages', () => {
 
       const duplicatedPage = await Page.findOne({ path: newPagePath });
       const duplicatedRevision = await Revision.findOne({ pageId: duplicatedPage._id });
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage).toBeTruthy();
       expect(duplicatedPage._id).not.toStrictEqual(_page._id);
       expect(duplicatedPage.grant).toBe(_page.grant);
@@ -1271,7 +1272,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage2 = await Page.findOne({ path: '/dup_np_duplicate2/np_duplicate3' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision2 = duplicatedPage2.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeTruthy();
       expect(duplicatedRevision1).toBeTruthy();
@@ -1316,7 +1317,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage3 = await Page.findOne({ path: '/dup_np_duplicate4/np_duplicate6' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision3 = duplicatedPage3.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeNull();
       expect(duplicatedPage3).toBeTruthy();
@@ -1352,7 +1353,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedPage2 = await Page.findOne({ path: '/dup_np_duplicate7/np_duplicate8' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedPage3 = await Page.findOne({ path: '/dup_np_duplicate7/np_duplicate9' }).populate({ path: 'revision', model: 'Revision' });
       const duplicatedRevision1 = duplicatedPage1.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeFalsy();
       expect(duplicatedPage3).toBeFalsy();
@@ -1394,7 +1395,7 @@ describe('PageService page operations with non-public pages', () => {
       const duplicatedRevision1 = duplicatedPage1.revision;
       const duplicatedRevision2 = duplicatedPage2.revision;
       const duplicatedRevision3 = duplicatedPage3.revision;
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage1).toBeTruthy();
       expect(duplicatedPage2).toBeTruthy();
       expect(duplicatedPage3).toBeTruthy();

+ 0 - 2
apps/app/test/integration/service/v5.page.test.ts

@@ -16,7 +16,6 @@ describe('Test page service methods', () => {
   let ShareLink;
   let PageRedirect;
   let PageOperation;
-  let xssSpy;
 
   let rootPage;
 
@@ -50,7 +49,6 @@ describe('Test page service methods', () => {
     /*
      * Common
      */
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
 
     // ***********************************************************************************************************
     // * Do NOT change properties of globally used documents. Otherwise, it might cause some errors in other tests

+ 17 - 16
apps/app/test/integration/service/v5.public-page.test.ts

@@ -5,6 +5,7 @@ import { PageActionType, PageActionStage } from '../../../src/interfaces/page-op
 import type { IPageTagRelation } from '../../../src/interfaces/page-tag-relation';
 import PageTagRelation from '../../../src/server/models/page-tag-relation';
 import Tag from '../../../src/server/models/tag';
+import { generalXssFilter } from '../../../src/services/general-xss-filter';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with only public pages', () => {
@@ -21,7 +22,7 @@ describe('PageService page operations with only public pages', () => {
   let ShareLink;
   let PageRedirect;
   let PageOperation;
-  let xssSpy;
+  let generalXssFilterProcessSpy;
 
   let rootPage;
 
@@ -62,7 +63,7 @@ describe('PageService page operations with only public pages', () => {
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser2 = await User.findOne({ username: 'v5DummyUser2' });
 
-    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
 
     rootPage = await Page.findOne({ path: '/' });
     if (rootPage == null) {
@@ -1254,7 +1255,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename1' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(childPageBeforeRename).toBeNull();
@@ -1275,7 +1276,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename2' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(parentPage.isEmpty).toBe(true);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
@@ -1296,7 +1297,7 @@ describe('PageService page operations with only public pages', () => {
         endpoint: '/_api/v3/pages/rename',
       });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(renamedPage.lastUpdateUser).toStrictEqual(dummyUser2._id);
@@ -1317,7 +1318,7 @@ describe('PageService page operations with only public pages', () => {
       });
       const pageRedirect = await PageRedirect.findOne({ fromPath: oldPath, toPath: renamedPage.path });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(pageRedirect).toBeTruthy();
@@ -1342,7 +1343,7 @@ describe('PageService page operations with only public pages', () => {
       const childPageBeforeRename = await Page.findOne({ path: '/v5_ChildForRename5' });
       const grandchildBeforeRename = await Page.findOne({ path: grandchild.path });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
       expect(childPageBeforeRename).toBeNull();
@@ -1369,7 +1370,7 @@ describe('PageService page operations with only public pages', () => {
       const grandchildAfterRename = await Page.findOne({ parent: renamedPage._id });
       const grandchildBeforeRename = await Page.findOne({ path: '/v5_ChildForRename7/v5_GrandchildForRename7' });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.isEmpty).toBe(true);
       expect(renamedPage.parent).toStrictEqual(parentPage._id);
@@ -1409,7 +1410,7 @@ describe('PageService page operations with only public pages', () => {
         endpoint: '/_api/v3/pages/rename',
       });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(renamedPage.path).toBe(newPath);
       expect(renamedPage.isEmpty).toBe(false);
       expect(renamedPage._id).toStrictEqual(page._id);
@@ -1715,7 +1716,7 @@ describe('PageService page operations with only public pages', () => {
       const baseRevision = await Revision.findOne({ pageId: page._id });
 
       // new path
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage._id).not.toStrictEqual(page._id);
       expect(duplicatedPage.revision).toStrictEqual(duplicatedRevision._id);
@@ -1751,7 +1752,7 @@ describe('PageService page operations with only public pages', () => {
       const baseRevision = await Revision.findOne({ pageId: page._id });
 
       // new path
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage._id).not.toStrictEqual(page._id);
       expect(duplicatedPage.revision).toStrictEqual(duplicatedRevision._id);
@@ -1789,7 +1790,7 @@ describe('PageService page operations with only public pages', () => {
       expect(revisionBodyForDupChild1).toBeTruthy();
       expect(revisionBodyForDupChild2).toBeTruthy();
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedChildPage1.path).toBe('/duplicatedv5PageForDuplicate3/v5_Child_1_ForDuplicate3');
       expect(duplicatedChildPage2.path).toBe('/duplicatedv5PageForDuplicate3/v5_Child_2_ForDuplicate3');
@@ -1809,7 +1810,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedChild = await Page.findOne({ parent: duplicatedPage._id });
       const duplicatedGrandchild = await Page.findOne({ parent: duplicatedChild._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage).toBeTruthy();
       expect(duplicatedGrandchild).toBeTruthy();
       expect(duplicatedPage.path).toBe(newPagePath);
@@ -1837,7 +1838,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedPage = await duplicate(basePage, newPagePath, dummyUser1, false);
       const duplicatedTagRelations = await PageTagRelation.find({ relatedPage: duplicatedPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedTagRelations.length).toBeGreaterThanOrEqual(2);
     });
@@ -1852,7 +1853,7 @@ describe('PageService page operations with only public pages', () => {
       const duplicatedPage = await duplicate(basePage, newPagePath, dummyUser1, false);
       const duplicatedComments = await Comment.find({ page: duplicatedPage._id });
 
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(basePageComments.length).not.toBe(duplicatedComments.length);
     });
@@ -1876,7 +1877,7 @@ describe('PageService page operations with only public pages', () => {
       expect(duplicatedGrandchild).toBeTruthy();
       expect(duplicatedChild.revision).toBeTruthy();
       expect(duplicatedGrandchild.revision).toBeTruthy();
-      expect(xssSpy).toHaveBeenCalled();
+      expect(generalXssFilterProcessSpy).toHaveBeenCalled();
       expect(duplicatedPage.path).toBe(newPagePath);
       expect(duplicatedPage.isEmpty).toBe(true);
       expect(duplicatedChild.revision.body).toBe(basePageChild.revision.body);

+ 0 - 1
apps/app/test/integration/setup-crowi.ts

@@ -13,7 +13,6 @@ const initCrowi = async(crowi) => {
 
   await Promise.all([
     crowi.setUpApp(),
-    crowi.setUpXss(),
   ]);
 
   await Promise.all([