Skip to content

Commit f0a1da3

Browse files
delucisnatemoo-re
andauthored
sanitize improvements (#79)
* Add test for blocking multiple elements * Fix sanitizer for nested blocked elements * Add new `unblockElements` option to sanitizer * Add bug fix changeset * Add feature changeset * Refactor * Cleanup --------- Co-authored-by: Nate Moore <[email protected]>
1 parent e8aee16 commit f0a1da3

File tree

5 files changed

+62
-3
lines changed

5 files changed

+62
-3
lines changed

.changeset/clean-pugs-learn.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'ultrahtml': patch
3+
---
4+
5+
Fixes sanitization of nested elements.
6+
7+
For example, the following code:
8+
9+
```js
10+
const output = await transform('<h1>Hello <strong>world!</strong></h1>', [
11+
sanitize({ blockElements: ['h1', 'strong'] }),
12+
]);
13+
```
14+
15+
produced the following output before this fix:
16+
17+
```html
18+
Hello <strong>world!</strong>
19+
```
20+
21+
and now correctly produces:
22+
23+
```html
24+
Hello world!
25+
```

.changeset/sharp-bears-cheat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ultrahtml": minor
3+
---
4+
5+
Adds a new `unblockElements` option to the `sanitize` transformer. This option makes it easier to remove all or most HTML from a string without dropping child content.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ console.log(output); // <h2>Hello world!</h2>
105105
| ------------------- | -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
106106
| allowElements | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be dropped. |
107107
| blockElements | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should remove, but keep their child elements. |
108+
| unblockElements | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be removed, but keep their child content. |
108109
| dropElements | `string[]` | `["script"]` | An array of strings indicating elements (including nested elements) that the sanitizer should remove. |
109110
| allowAttributes | `Record<string, string[]>` | `undefined` | An object where each key is the attribute name and the value is an Array of allowed tag names. Matching attributes will not be removed. All attributes that are not in the array will be dropped. |
110111
| dropAttributes | `Record<string, string[]>` | `undefined` | An object where each key is the attribute name and the value is an Array of dropped tag names. Matching attributes will be removed. |

src/transformers/sanitize.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ElementNode, ELEMENT_NODE, Node, walkSync } from '../index.js';
33
export interface SanitizeOptions {
44
/** An Array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be dropped. */
55
allowElements?: string[];
6+
/** An Array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be removed while keeping their child content. */
7+
unblockElements?: string[];
68
/** An Array of strings indicating elements that the sanitizer should remove, but keeping their child elements. */
79
blockElements?: string[];
810
/** An Array of strings indicating elements (including nested elements) that the sanitizer should remove. */
@@ -73,7 +75,9 @@ function getAction(
7375
}
7476
if (kind === 'component' && !sanitize.allowComponents) return 'drop';
7577
if (kind === 'custom-element' && !sanitize.allowCustomElements) return 'drop';
76-
78+
if (sanitize.unblockElements) {
79+
return sanitize.unblockElements.some((n) => n === name) ? 'allow' : 'block';
80+
}
7781
return sanitize.allowElements?.length > 0 ? 'drop' : 'allow';
7882
}
7983

@@ -141,8 +145,9 @@ export default function sanitize(opts?: SanitizeOptions) {
141145
return;
142146
}
143147
});
144-
for (const action of actions) {
145-
action();
148+
// Execute actions in reverse order so that children are mutated before parents.
149+
for (let i = actions.length - 1; i >= 0; i--) {
150+
actions[i]();
146151
}
147152
return doc;
148153
};

test/transformers/sanitize.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,36 @@ describe('sanitize', () => {
1515
]);
1616
expect(output).toEqual('Hello world!');
1717
});
18+
it('block with multiple elements', async () => {
19+
const input = `<h1>Hello <strong>world!</strong></h1>`;
20+
const output = await transform(input, [
21+
sanitize({ blockElements: ['h1', 'strong'] }),
22+
]);
23+
expect(output).toEqual('Hello world!');
24+
});
1825
it('allow', async () => {
1926
const input = `<script>console.log("pwnd")</script>`;
2027
const output = await transform(input, [
2128
sanitize({ allowElements: ['script'] }),
2229
]);
2330
expect(output).toEqual('<script>console.log("pwnd")</script>');
2431
});
32+
describe('unblock', () => {
33+
it('empty unblock array blocks all elements', async () => {
34+
const input = `<h1>Hello <strong>world!</strong></h1>`;
35+
const output = await transform(input, [
36+
sanitize({ unblockElements: [] }),
37+
]);
38+
expect(output).toEqual('Hello world!');
39+
});
40+
it('unblock array blocks unlisted elements', async () => {
41+
const input = `<h1>Hello <strong>world!</strong></h1>`;
42+
const output = await transform(input, [
43+
sanitize({ unblockElements: ['strong'] }),
44+
]);
45+
expect(output).toEqual('Hello <strong>world!</strong>');
46+
});
47+
});
2548
it('allow drops everything else', async () => {
2649
const input = `<h1>Hello world!</h1><h4>This is not allowed</h4>`;
2750
const output = await transform(input, [

0 commit comments

Comments
 (0)