Skip to content
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

Refactor sanitizeHTML method to support attributes #120

Merged
merged 10 commits into from
Oct 17, 2023
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ It is also possible to provide custom allowed tags directly to the directive tag
</template>
```

#### allowedAttributes

Array of strings. Default: []

Customize the tag attributes that are allowed to be rendered:

```js
Vue.use(VueSafeHTML, {
allowedTags: ['a'],
allowedAttributes: ['title', 'class', 'href'],
});

### Nuxt

`vue-safe-html` is written as a Vue plugin so you can easily use it inside Nuxt by following [the Nuxt documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins#vue-plugins).
Expand Down
6 changes: 4 additions & 2 deletions src/directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ const defaultTags = [
'sub',
'sup',
];

const areTagsValid = (tags) => (
Array.isArray(tags) &&
tags.every((tag) => typeof tag === 'string')
);

export { defaultTags as allowedTags };

export default (tags) => {
export default (tags, attributes) => {
const initialTags = areTagsValid(tags) ? tags : defaultTags;

return (el, binding) => {
let finalTags = initialTags;

Expand All @@ -33,7 +35,7 @@ export default (tags) => {
}
}

const sanitized = sanitizeHTML(binding.value, finalTags);
const sanitized = sanitizeHTML(binding.value, finalTags, attributes);

if (typeof el.innerHTML === 'string') {
// we're client-side and `el` is an HTMLElement
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export {

export default {
install: (Vue, options = {}) => {
Vue.directive('safe-html', createDirective(options.allowedTags));
Vue.directive('safe-html', createDirective(options.allowedTags, options.allowedAttributes));
},
};
55 changes: 27 additions & 28 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
// Strips all tags
const removeAllTagsRegex = /<\/?[^>]+(>|$)/g;
export const removeAllTags = (input) => (input.replace(removeAllTagsRegex, ''));

/**
* sanitizeHTML strips html tags in the given string
* if allowedTags is empty, all tags are stripped
* @param {*} htmlString the HTML strings
* @param {*} allowedTags array of tags that are not stripped
*/
// eslint-disable-next-line import/prefer-default-export
export const sanitizeHTML = (htmlString, allowedTags = []) => {
// Add an optional white space to the allowed tags
const allowedTagsWhiteSpaced = allowedTags.map((tag) => `${tag}\\s*`);

// Remove tag attributes
// The solution for this was found on:
// https://stackoverflow.com/questions/4885891/regex-for-removing-all-attributes-from-a-paragraph
const htmlWithoutAttributes = htmlString.replace(/<(\w+)(.|[\r\n])*?>/g, '<$1>');
export const sanitizeHTML = (htmlString, allowedTags = [], allowedAttributes = []) => {
if (!htmlString) {
return '';
}

const expression = (allowedTags.length > 0) ?
// Regex explanation
// Note: \ needs to be escaped in the final expression
// '<' Match the starting tag
// '(' Create a matching group
// '?!' Use negative lookup
// we only want to match the tags that are not in the allowedTags array
// '\s*?' Optional match of any white space charater before optional /
// '\/?' Matches / zero to one time for the closing tag
// '\s*?' Optional match of any white space charater after optional /
// '(${allowedTags.join('|')})>' matching group of the allowed tags
// ')' close the matching group of negative lookup
// '\w*[^<>]*' matches any word that isn't in the excluded group
// '>' Match closing tagq
`<(?!\\s*\\/?\\s*(${allowedTagsWhiteSpaced.join('|')})>)\\w*[^<>]*>` :
// Strips all tags
'<(\\/?\\w*)\\w*[^<>]*>';
if(allowedTags.length === 0) {
LostCrew marked this conversation as resolved.
Show resolved Hide resolved
return removeAllTags(htmlString);
}

const regExp = new RegExp(expression, 'gm');
return htmlWithoutAttributes.replace(regExp, '');
return htmlString.replace(/<(\/*)(\w+)([^>]*)>/g, (match, closing, tagName, attrs) => {
if (allowedTags.includes(tagName)) {
// If the tag is allowed, we'll retain only allowed attributes.
if (closing) {
// If it's a closing tag, simply return it as is.
return `</${tagName}>`;
}
// Otherwise, reconstruct the opening tag with only allowed attributes.
let allowedAttrs = attrs.split(/\s+/)
LostCrew marked this conversation as resolved.
Show resolved Hide resolved
.filter(attr => allowedAttributes.includes(attr.split('=')[0]))
.join(' ');
return `<${tagName}${allowedAttrs ? ' ' + allowedAttrs : ''}>`;
axlwaii marked this conversation as resolved.
Show resolved Hide resolved
}
// If the tag is not allowed, strip it completely.
return '';
});
};
30 changes: 30 additions & 0 deletions src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,35 @@ describe('Utils', () => {
const expected = '<p>Test1</p> Test2';
expect(utils.sanitizeHTML(given, allowedTags)).toBe(expected);
});

it('Keeps allowed attributes', () => {
const allowedTags = ['p', 'strong'];
const allowedAttributes = ['title'];
const given = '<p data-test="test" title="test2">Test1</p> <strong data-test=\'test2\'>Test2</strong>';
const expected = '<p title="test2">Test1</p> <strong>Test2</strong>';
expect(utils.sanitizeHTML(given, allowedTags, allowedAttributes)).toBe(expected);
});

it('ignores incomplete tag', () => {
axlwaii marked this conversation as resolved.
Show resolved Hide resolved
const allowedTags = ['p', 'strong'];
const allowedAttributes = ['data-lazy'];
const given = '<p data-lazy="test">Test1</p> <adsfjgsa>with invalid tag </';
const expected = '<p data-lazy="test">Test1</p> with invalid tag </';
expect(utils.sanitizeHTML(given, allowedTags, allowedAttributes)).toBe(expected);
});

it('Does not crash on null input', () => {
axlwaii marked this conversation as resolved.
Show resolved Hide resolved
const allowedTags = [];
const given = null;
const expected = '';
expect(utils.sanitizeHTML(given, allowedTags)).toBe(expected);
});

it('Does not crash on undefined input', () => {
const allowedTags = [];
const given = undefined;
const expected = '';
expect(utils.sanitizeHTML(given, allowedTags)).toBe(expected);
});
});
});