Skip to content

Commit

Permalink
Support "target=_blank" in A links via clean-html directive (rancher#…
Browse files Browse the repository at this point in the history
…9928)

* Allow target=_blank on A tags through DOMPurify

* Remove unused params

* Fix lint issue

* Prevent directives from being overwritten

* Fix lint issues
  • Loading branch information
nwmac authored Nov 23, 2023
1 parent 526d347 commit d5dccb4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 24 deletions.
28 changes: 24 additions & 4 deletions shell/initialize/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,16 @@ import { setContext, getLocation, getRouteData, normalizeError } from '../utils/
import { createStore } from '../config/store.js';

/* Plugins */

import { loadDirectives } from '@shell/plugins';
import '../plugins/portal-vue.js';
import cookieUniversalNuxt from '../utils/cookie-universal-nuxt.js';
import axios from '../utils/axios.js';
import plugins from '../core/plugins.js';
import pluginsLoader from '../core/plugins-loader.js';
import axiosShell from '../plugins/axios';
import '../plugins/tooltip';
import '../plugins/clean-tooltip-directive';
import '../plugins/vue-clipboard2';
import '../plugins/v-select';
import '../plugins/directives';
import '../plugins/clean-html-directive';
import '../plugins/transitions';
import '../plugins/vue-js-modal';
import '../plugins/js-yaml';
Expand All @@ -47,6 +44,29 @@ import '../plugins/formatters';
import version from '../plugins/version';
import steveCreateWorker from '../plugins/steve-create-worker';

// Prevent extensions from overriding existing directives
// Hook into Vue.directive and keep track of the directive names that have been added
// and prevent an existing directive from being overwritten
const directiveNames = {};
const vueDirective = Vue.directive;

Vue.directive = function(name) {
if (directiveNames[name]) {
console.log(`Can not override directive: ${ name }`); // eslint-disable-line no-console

return;
}

directiveNames[name] = true;

vueDirective.apply(Vue, arguments);
};

// Load the directives from the plugins - we do this with a function so we know
// these are initialized here, after the code above which keeps track of them and
// prevents over-writes
loadDirectives();

// Component: <ClientOnly>
Vue.component(ClientOnly.name, ClientOnly);

Expand Down
20 changes: 1 addition & 19 deletions shell/plugins/clean-html-directive.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import Vue from 'vue';
import DOMPurify from 'dompurify';

const ALLOWED_TAGS = [
'code',
'li',
'a',
'p',
'b',
'br',
'ul',
'pre',
'span',
'div',
'i',
'em',
'strong',
];

export const purifyHTML = (value) => DOMPurify.sanitize(value, { ALLOWED_TAGS });
import { purifyHTML } from './clean-html';

export const cleanHtmlDirective = {
inserted(el, binding) {
Expand Down
40 changes: 40 additions & 0 deletions shell/plugins/clean-html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import DOMPurify from 'dompurify';
import { uniq } from '@shell/utils/array';

const ALLOWED_TAGS = [
'code',
'li',
'a',
'p',
'b',
'br',
'ul',
'pre',
'span',
'div',
'i',
'em',
'strong',
];

// Allow 'A' tags to keep the target=_blank attribute if they have it
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
if (node.tagName === 'A' && data.attrName === 'target' && data.attrValue === '_blank') {
data.forceKeepAttr = true;
}
});

// Ensure if an 'A' tag has target=_blank that we add noopener, noreferrer and nofollow to the 'rel' attribute
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node?.target === '_blank') {
const rel = ['noopener', 'noreferrer', 'nofollow'];
const existingRel = node.rel?.length ? node.rel.split(' ') : [];
const combined = uniq([...rel, ...existingRel]);

node.setAttribute('rel', combined.join(' '));
}
});

export const purifyHTML = (value, options = { ALLOWED_TAGS }) => {
return DOMPurify.sanitize(value, options);
};
2 changes: 1 addition & 1 deletion shell/plugins/clean-tooltip-directive.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { VTooltip } from 'v-tooltip';
import { purifyHTML } from './clean-html-directive';
import { purifyHTML } from './clean-html';

function purifyContent(value) {
const type = typeof value;
Expand Down
11 changes: 11 additions & 0 deletions shell/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Load the directives
*
* These are included in a function that can be explictly called, so that we can be sure
* of the execution order, rather than importing them at the top of a file.
*/
export function loadDirectives() {
import('./clean-html-directive');
import('./clean-tooltip-directive');
import('./directives');
}

0 comments on commit d5dccb4

Please sign in to comment.