Add DomPurify sanitizer custom elements configuration (#26989)
* Add DomPurify sanitizer custom elements configuration --------- Signed-off-by: Harsha Teja Kanna <h7kanna@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9a6d61c9c9
commit
4f0cb89c42
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Added DomPurify sanitizer configuration for custom elements implementing RFC https://github.com/backstage/backstage/issues/26988.
|
||||
See https://backstage.io/docs/features/techdocs/how-to-guides#how-to-enable-custom-elements-in-techdocs for how to enable it in the configuration.
|
||||
@@ -545,6 +545,26 @@ techdocs:
|
||||
This way, all iframes where the host in the src attribute is in the
|
||||
`sanitizer.allowedIframeHosts` list will be displayed.
|
||||
|
||||
## How to enable custom elements in TechDocs
|
||||
|
||||
TechDocs uses the [DOMPurify](https://github.com/cure53/DOMPurify) library to
|
||||
sanitize HTML and prevent XSS attacks.
|
||||
|
||||
It's possible to allow custom elements based on a list of allowed patterns. To do
|
||||
this, add the allowed elements and attributes in the `techdocs.sanitizer.allowedCustomElementTagNameRegExp`
|
||||
and `allowedCustomElementAttributeNameRegExp` configuration of your `app-config.yaml`.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
techdocs:
|
||||
sanitizer:
|
||||
allowedCustomElementTagNameRegExp: '^backstage-',
|
||||
allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2',
|
||||
```
|
||||
|
||||
This way, custom element like `<backstage-element attribute1="value"></backstage-element>` will be allowed in the result HTML.
|
||||
|
||||
## How to render PlantUML diagram in TechDocs
|
||||
|
||||
PlantUML allows you to create diagrams from plain text language. Each diagram description begins with the keyword - (@startXYZ and @endXYZ, depending on the kind of diagram). For UML Diagrams, Keywords @startuml & @enduml should be used. Further details for all types of diagrams can be found at [PlantUML Language Reference Guide](https://plantuml.com/guide).
|
||||
|
||||
Vendored
+16
@@ -42,6 +42,22 @@ export interface Config {
|
||||
* @visibility frontend
|
||||
*/
|
||||
allowedIframeHosts?: string[];
|
||||
/**
|
||||
* Allows listed custom element tag name regex
|
||||
* Example:
|
||||
* allowedCustomElementTagNameRegExp: '^backstage-'
|
||||
* this will allow all custom elements with tag name matching `^backstage-` like <backstage-custom-element /> etc.
|
||||
* @visibility frontend
|
||||
*/
|
||||
allowedCustomElementTagNameRegExp: string;
|
||||
/**
|
||||
* Allows listed custom element attribute name regex
|
||||
* Example:
|
||||
* allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2'
|
||||
* this will allow all custom element attributes matching `attribute1` or `attribute2` like <backstage-custom-element attribute1="yes" attribute2/>
|
||||
* @visibility frontend
|
||||
*/
|
||||
allowedCustomElementAttributeNameRegExp: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { useSanitizerTransformer } from './transformer';
|
||||
|
||||
const configApiMock: ConfigApi = new ConfigReader({
|
||||
techdocs: {
|
||||
sanitizer: {
|
||||
allowedCustomElementTagNameRegExp: '^backstage-',
|
||||
allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper: FC<PropsWithChildren<{}>> = ({ children }) => (
|
||||
<TestApiProvider apis={[[configApiRef, configApiMock]]}>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
|
||||
describe('Transformers > Html > Sanitizer Custom Elements', () => {
|
||||
it('should return a function that allows custom elements matching the pattern in the given dom element', async () => {
|
||||
const { result } = renderHook(() => useSanitizerTransformer(), { wrapper });
|
||||
|
||||
const dirtyDom = document.createElement('html');
|
||||
dirtyDom.innerHTML = `
|
||||
<body>
|
||||
<backstage-element attribute1="test" attribute2></backstage-element>
|
||||
</body>
|
||||
`;
|
||||
const clearDom = await result.current(dirtyDom); // calling html transformer
|
||||
|
||||
const elements = Array.from(
|
||||
clearDom.querySelectorAll<HTMLElement>('body > backstage-element'),
|
||||
);
|
||||
expect(elements).toHaveLength(1);
|
||||
expect(elements[0].hasAttribute('attribute1')).toEqual(true);
|
||||
expect(elements[0].hasAttribute('attribute2')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return a function that removes custom elements not matching the pattern in the given dom element', async () => {
|
||||
const { result } = renderHook(() => useSanitizerTransformer(), { wrapper });
|
||||
|
||||
const dirtyDom = document.createElement('html');
|
||||
dirtyDom.innerHTML = `
|
||||
<body>
|
||||
<backstage-element attribute1="test" attribute2></backstage-element>
|
||||
<invalid-element attribute1="test" attribute2></invalid-element>
|
||||
</body>
|
||||
`;
|
||||
const clearDom = await result.current(dirtyDom); // calling html transformer
|
||||
|
||||
const elements = Array.from(
|
||||
clearDom.querySelectorAll<HTMLElement>('body > backstage-element'),
|
||||
);
|
||||
expect(elements).toHaveLength(1);
|
||||
expect(elements[0].hasAttribute('attribute1')).toEqual(true);
|
||||
expect(elements[0].hasAttribute('attribute2')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return a function that removes custom element attributes not matching the pattern in the given dom element', async () => {
|
||||
const { result } = renderHook(() => useSanitizerTransformer(), { wrapper });
|
||||
|
||||
const dirtyDom = document.createElement('html');
|
||||
dirtyDom.innerHTML = `
|
||||
<body>
|
||||
<backstage-element attribute1="test" attribute2></backstage-element>
|
||||
<backstage-element attribute3="test" attribute4></backstage-element>
|
||||
</body>
|
||||
`;
|
||||
const clearDom = await result.current(dirtyDom); // calling html transformer
|
||||
|
||||
const elements = Array.from(
|
||||
clearDom.querySelectorAll<HTMLElement>('body > backstage-element'),
|
||||
);
|
||||
expect(elements).toHaveLength(2);
|
||||
expect(elements[0].hasAttribute('attribute1')).toEqual(true);
|
||||
expect(elements[0].hasAttribute('attribute2')).toEqual(true);
|
||||
expect(elements[1].hasAttribute('attribute3')).toEqual(false);
|
||||
expect(elements[1].hasAttribute('attribute4')).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,13 @@ export const useSanitizerTransformer = (): Transformer => {
|
||||
}
|
||||
});
|
||||
|
||||
const tagNameCheck = config?.getOptionalString(
|
||||
'allowedCustomElementTagNameRegExp',
|
||||
);
|
||||
const attributeNameCheck = config?.getOptionalString(
|
||||
'allowedCustomElementAttributeNameRegExp',
|
||||
);
|
||||
|
||||
// using outerHTML as we want to preserve the html tag attributes (lang)
|
||||
return DOMPurify.sanitize(dom.outerHTML, {
|
||||
ADD_TAGS: tags,
|
||||
@@ -79,6 +86,12 @@ export const useSanitizerTransformer = (): Transformer => {
|
||||
ADD_ATTR: ['http-equiv', 'content'],
|
||||
WHOLE_DOCUMENT: true,
|
||||
RETURN_DOM: true,
|
||||
CUSTOM_ELEMENT_HANDLING: {
|
||||
tagNameCheck: tagNameCheck ? new RegExp(tagNameCheck) : undefined,
|
||||
attributeNameCheck: attributeNameCheck
|
||||
? new RegExp(attributeNameCheck)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
[config],
|
||||
|
||||
Reference in New Issue
Block a user