Skip to content

feat(security): Strip HTML script tags before inserting content into DOM. Fixes #1974,#1665 #2134

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

Merged
merged 22 commits into from
Nov 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/components/button-group/button-group.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { mergeData } from 'vue-functional-data-merge'
import { arrayIncludes } from '../../utils/array'

export const props = {
vertical: {
Expand All @@ -8,8 +7,7 @@ export const props = {
},
size: {
type: String,
default: null,
validator: size => arrayIncludes(['sm', '', 'lg'], size)
default: null
},
tag: {
type: String,
Expand Down
5 changes: 3 additions & 2 deletions src/components/card/card-body.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mergeData } from 'vue-functional-data-merge'
import prefixPropName from '../../utils/prefix-prop-name'
import copyProps from '../../utils/copyProps'
import { assign } from '../../utils/object'
import stripScripts from '../../utils/strip-scripts'
import cardMixin from '../../mixins/card-mixin'

export const props = assign(
Expand Down Expand Up @@ -47,14 +48,14 @@ export default {
if (props.title) {
cardTitle = h(props.titleTag, {
staticClass: 'card-title',
domProps: { innerHTML: props.title }
domProps: { innerHTML: stripScripts(props.title) }
})
}

if (props.subTitle) {
cardSubTitle = h(props.subTitleTag, {
staticClass: 'card-subtitle mb-2 text-muted',
domProps: { innerHTML: props.subTitle }
domProps: { innerHTML: stripScripts(props.subTitle) }
})
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/dropdown/dropdown.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import idMixin from '../../mixins/id'
import dropdownMixin from '../../mixins/dropdown'
import stripScripts from '../../utils/strip-scripts'
import bButton from '../button/button'

// Needed when dropdowns are inside an input group
Expand Down Expand Up @@ -27,7 +28,7 @@ export default {
click: this.click
}
},
[this.$slots['button-content'] || this.$slots.text || this.text]
[this.$slots['button-content'] || this.$slots.text || stripScripts(this.text)]
)
}
const toggle = h(
Expand All @@ -54,7 +55,7 @@ export default {
[
this.split
? h('span', { class: ['sr-only'] }, [this.toggleText])
: this.$slots['button-content'] || this.$slots.text || this.text
: this.$slots['button-content'] || this.$slots.text || stripScripts(this.text)
]
)
const menu = h(
Expand Down
2 changes: 1 addition & 1 deletion src/components/embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Any additional attributes provided to `<b-embed>` (other than the above `type`,
Any children elements between the opening and closing `<b-embed>` will be placed
inside the inner embeded element. Note that type `iframe` does not support any children.

**Example: Responsive embeding of an HTML5 `<video>`**
**Example: Responsive embedding of an HTML5 `<video>`**
```html
<b-embed type="video" aspect="4by3" controls poster="poster.png">
<source src="devstories.webm"
Expand Down
9 changes: 5 additions & 4 deletions src/components/form-group/form-group.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import warn from '../../utils/warn'
import stripScripts from '../../utils/strip-scripts'
import { select, selectAll, isVisible, setAttr, removeAttr, getAttr } from '../../utils/dom'
import idMixin from '../../mixins/id'
import formStateMixin from '../../mixins/form-state'
Expand All @@ -21,7 +22,7 @@ export default {
if (this.hasLabel) {
let children = $slots['label']
const legendTag = this.labelFor ? 'label' : 'legend'
const legendDomProps = children ? {} : { innerHTML: this.label }
const legendDomProps = children ? {} : { innerHTML: stripScripts(this.label) }
const legendAttrs = { id: this.labelId, for: this.labelFor || null }
const legendClick = (this.labelFor || this.labelSrOnly) ? {} : { click: this.legendClick }
if (this.horizontal) {
Expand Down Expand Up @@ -69,7 +70,7 @@ export default {
if (this.hasInvalidFeedback) {
let domProps = {}
if (!$slots['invalid-feedback'] && !$slots['feedback']) {
domProps = { innerHTML: this.invalidFeedback || this.feedback || '' }
domProps = { innerHTML: stripScripts(this.invalidFeedback || this.feedback || '') }
}
invalidFeedback = h(
'b-form-invalid-feedback',
Expand All @@ -92,7 +93,7 @@ export default {
// Valid feeback text (explicitly hidden if state is invalid)
let validFeedback = h(false)
if (this.hasValidFeedback) {
const domProps = $slots['valid-feedback'] ? {} : { innerHTML: this.validFeedback || '' }
const domProps = $slots['valid-feedback'] ? {} : { innerHTML: stripScripts(this.validFeedback || '') }
validFeedback = h(
'b-form-valid-feedback',
{
Expand All @@ -114,7 +115,7 @@ export default {
// Form help text (description)
let description = h(false)
if (this.hasDescription) {
const domProps = $slots['description'] ? {} : { innerHTML: this.description || '' }
const domProps = $slots['description'] ? {} : { innerHTML: stripScripts(this.description || '') }
description = h(
'b-form-text',
{ attrs: { id: this.descriptionId }, domProps: domProps },
Expand Down
5 changes: 3 additions & 2 deletions src/components/input-group/input-group.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mergeData } from 'vue-functional-data-merge'
import stripScripts from '../../utils/strip-scripts'
import InputGroupPrepend from './input-group-prepend'
import InputGroupAppend from './input-group-append'
import InputGroupText from './input-group-text'
Expand Down Expand Up @@ -40,7 +41,7 @@ export default {
if (props.prepend) {
childNodes.push(
h(InputGroupPrepend, [
h(InputGroupText, { domProps: { innerHTML: props.prepend } })
h(InputGroupText, { domProps: { innerHTML: stripScripts(props.prepend) } })
])
)
} else {
Expand All @@ -65,7 +66,7 @@ export default {
if (props.append) {
childNodes.push(
h(InputGroupAppend, [
h(InputGroupText, { domProps: { innerHTML: props.append } })
h(InputGroupText, { domProps: { innerHTML: stripScripts(props.append) } })
])
)
} else {
Expand Down
5 changes: 3 additions & 2 deletions src/components/jumbotron/jumbotron.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mergeData } from 'vue-functional-data-merge'
import stripScripts from '../../utils/strip-scripts'
import Container from '../layout/container'

export const props = {
Expand Down Expand Up @@ -66,7 +67,7 @@ export default {
[`display-${props.headerLevel}`]: Boolean(props.headerLevel)
}
},
$slots.header || props.header
$slots.header || stripScripts(props.header)
))
}

Expand All @@ -75,7 +76,7 @@ export default {
childNodes.push(h(
props.leadTag,
{ staticClass: 'lead' },
$slots.lead || props.lead
$slots.lead || stripScripts(props.lead)
))
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import observeDom from '../../utils/observe-dom'
import warn from '../../utils/warn'
import KeyCodes from '../../utils/key-codes'
import BvEvent from '../../utils/bv-event.class'
import stripScripts from '../../utils/strip-scripts'

import {
isVisible,
Expand Down Expand Up @@ -70,7 +71,7 @@ export default {
}
modalHeader = [
h(this.titleTag, { class: ['modal-title'] }, [
$slots['modal-title'] || this.title
$slots['modal-title'] || stripScripts(this.title)
]),
closeButton
]
Expand Down Expand Up @@ -116,7 +117,7 @@ export default {
}
}
},
[$slots['modal-cancel'] || this.cancelTitle]
[$slots['modal-cancel'] || stripScripts(this.cancelTitle)]
)
}
const okButton = h(
Expand All @@ -133,7 +134,7 @@ export default {
}
}
},
[$slots['modal-ok'] || this.okTitle]
[$slots['modal-ok'] || stripScripts(this.okTitle)]
)
modalFooter = [cancelButton, okButton]
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/nav/nav-item-dropdown.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import idMixin from '../../mixins/id'
import dropdownMixin from '../../mixins/dropdown'
import stripScripts from '../../utils/strip-scripts'

export default {
mixins: [idMixin, dropdownMixin],
Expand All @@ -24,7 +25,7 @@ export default {
[
this.$slots['button-content'] ||
this.$slots.text ||
h('span', { domProps: { innerHTML: this.text } })
h('span', { domProps: { innerHTML: stripScripts(this.text) } })
]
)
const menu = h(
Expand Down
4 changes: 3 additions & 1 deletion src/components/progress/progress-bar.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import stripScripts from '../../utils/strip-scripts'

export default {
render (h) {
let childNodes = h(false)
if (this.$slots.default) {
childNodes = this.$slots.default
} else if (this.label) {
childNodes = h('span', { domProps: { innerHTML: this.label } })
childNodes = h('span', { domProps: { innerHTML: stripScripts(this.label) } })
} else if (this.computedShowProgress) {
childNodes = this.progress.toFixed(this.computedPrecision)
} else if (this.computedShowValue) {
Expand Down
17 changes: 11 additions & 6 deletions src/components/table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ the `row-clicked` event:_

#### Displaying raw HTML
By default `b-table` escapes HTML tags in items, if you need to display raw HTML code in `b-table`, you should use
`v-html` prop in scoped field slot
`v-html` directive on an element in a in scoped field slot

```html
<template>
Expand Down Expand Up @@ -727,21 +727,26 @@ export default {
<!-- table-html-data-slots.vue -->
```

**Note:** Be cautious of using this to display user supplied content, as script
tags could be injected into your page!


### Formatter callback
One more option to customize field output is to use formatter callback function.
To enable this field's property `formatter` is used. Value of this property may be
String or function reference. In case of a String value, function must be defined at
parent component's methods. Providing formatter as `Function`, it must be declared at
global scope (window or as global mixin at Vue).
String or function reference. In case of a String value, the function must be defined at
the parent component's methods. Providing formatter as a `Function`, it must be declared at
global scope (window or as global mixin at Vue), unless it has been bound to a `this` context.

Callback function accepts three arguments - `value`, `key`, and `item`, and should
return the formatted value as a string (basic HTML is supported)
The callback function accepts three arguments - `value`, `key`, and `item`, and should
return the formatted value as a string (HTML strings are not supported)

**Example: Custom data rendering with formatter callback function**
```html
<template>
<b-table :fields="fields" :items="items">
<template slot="name" slot-scope="data">
<!-- data.value is th value after formattetd by the Formatter -->
<a :href="`#${data.value.replace(/[^a-z]+/i,'-').toLowerCase()}`">
{{data.value}}
</a>
Expand Down
9 changes: 5 additions & 4 deletions src/components/table/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import looseEqual from '../../utils/loose-equal'
import stableSort from '../../utils/stable-sort'
import KeyCodes from '../../utils/key-codes'
import warn from '../../utils/warn'
import stripScripts from '../../utils/strip-scripts'
import { keys, assign } from '../../utils/object'
import { arrayIncludes, isArray } from '../../utils/array'
import idMixin from '../../mixins/id'
Expand Down Expand Up @@ -110,7 +111,7 @@ export default {
if (this.caption || $slots['table-caption']) {
const data = { style: this.captionStyles }
if (!$slots['table-caption']) {
data.domProps = { innerHTML: this.caption }
data.domProps = { innerHTML: stripScripts(this.caption) }
}
caption = h('caption', data, $slots['table-caption'])
}
Expand Down Expand Up @@ -165,7 +166,7 @@ export default {
if (slot) {
slot = [slot({ label: field.label, column: field.key, field: field })]
} else {
data.domProps = { innerHTML: field.label }
data.domProps = { innerHTML: stripScripts(field.label) }
}
return h('th', data, slot)
})
Expand Down Expand Up @@ -246,7 +247,7 @@ export default {
} else {
const formatted = this.getFormattedValue(item, field)
if (this.isStacked) {
// We innerHTML a DIV to ensure rendered as a single cell when visually stacked!
// We wrap in a DIV to ensure rendered as a single cell when visually stacked!
childNodes = [h('div', formatted)]
} else {
// Non stacked
Expand Down Expand Up @@ -336,7 +337,7 @@ export default {
if (!empty) {
empty = h('div', {
class: ['text-center', 'my-2'],
domProps: { innerHTML: this.filter ? this.emptyFilteredText : this.emptyText }
domProps: { innerHTML: stripScripts(this.filter ? this.emptyFilteredText : this.emptyText) }
})
}
empty = h(
Expand Down
9 changes: 5 additions & 4 deletions src/mixins/form-options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isArray } from '../utils/array'
import { keys } from '../utils/object'
import stripScripts from '../utils/strip-scripts'

function isObject (obj) {
return obj && ({}).toString.call(obj) === '[object Object]'
Expand Down Expand Up @@ -41,13 +42,13 @@ export default {
if (isObject(option)) {
return {
value: option[valueField],
text: String(option[textField]),
text: stripScripts(String(option[textField])),
disabled: option[disabledField] || false
}
}
return {
value: option,
text: String(option),
text: stripScripts(String(option)),
disabled: false
}
})
Expand All @@ -61,13 +62,13 @@ export default {
const text = option[textField]
return {
value: typeof value === 'undefined' ? key : value,
text: typeof text === 'undefined' ? key : String(text),
text: typeof text === 'undefined' ? key : stripScripts(String(text)),
disabled: option[disabledField] || false
}
}
return {
value: key,
text: String(option),
text: stripScripts(String(option)),
disabled: false
}
})
Expand Down
Loading