Skip to content

Commit

Permalink
Merge pull request #1419 from lucasnetau/purify
Browse files Browse the repository at this point in the history
Implement built-in XSS and DOM Clobbering protection
  • Loading branch information
kevinchappell authored Oct 5, 2023
2 parents 843eca7 + c75ccdb commit 57bf6e6
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/formBuilder/controls.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Control architecture

Controls defined in this directory will be transpiled into the core `formBuilder` & `formRender` plugins. Only 'core' regularly used plugins should be included here. Plugins that have less common use-cases should be added as [control plugins](control-plugins) which are only loaded as required.
Controls defined in this directory will be transpiled into the core `formBuilder` & `formRender` plugins. Only 'core' regularly used plugins should be included here. Plugins that have less common use-cases should be added as [control plugins](control-plugins.md) which are only loaded as required.

All control classes should inherit from `src/js/control.js`. Each class can support one or more `types` or `subtypes`.

Expand Down
2 changes: 1 addition & 1 deletion docs/formBuilder/options/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ $(container).formBuilder(options);
## See it in Action
<p data-height="494" data-theme-id="22927" data-embed-version="2" data-slug-hash="rmxYVW" data-default-tab="result" data-user="kevinchappell" class="codepen"></p>

For a more advanced example see: [Demos->Translation](/demos/translation/)
For a more advanced example see: [Demos->Translation](../demos/translation.md)
61 changes: 61 additions & 0 deletions docs/formBuilder/options/sanitizerOptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SanitizerOptions
`sanitizerOptions` provides the configuration of the built-in script injection, DOM Clobbering and Form hijacking protection.

This protection is disabled by default, however should be enabled when any of the following apply:
* Input into fields may be copy/pasted from untrusted sources (especially Label field or paragraph Content field)
* Untrusted users may build forms

In a future version of FormBuilder protections may be enabled by default.

_Script injection_ protection will remove `<script>` elements, inline javascript, and on* event attributes from FormElements when rendering the FormBuilder previews and FormRender forms. Additionally invalid or incomplete HTML will be cleaned up.

_DOM Clobbering_ protection will remove _id_ and _name_ attribute values which cause attributes in the Document and Form DOM objects to be overwritten.

_Form Protection_ will ensure than buttons cannot override the form action nor act upon another form.

## Enabling protections
```javascript
const sanitizerOptions = {
clobberingProtection: {
document: true,
form: false, //Set true for FormRender
},
backendOrder: ['dompurify','sanitizer','fallback'],
};
$(container).formBuilder(options);
```

## Sanitizer backends

FormBuilder supports three Sanitizer backends:
- DomPurify
- Sanitizer API
- jQuery based fallback

### DomPurify
To enable support for the DomPurify backend the Javascript library should be included before FormBuilder is included on your page.

Information on installing DomPurify can be found on the project page https://github.com/cure53/DOMPurify

### Sanitizer API
Sanitizer API is an experimental web feature being implemented by the major web browsers. The Sanitizer backend will use this API if it is detected in the browser.

### jQuery based fallback
A built-in fallback method is provided when DomPurify an Sanitizer API is not enabled or available.

## DOM Clobbering
DOM clobbering prevention can be enabled to protect the attributes of the global document dom element and any wrapping `<form>` element.

Optionally instead of removing offending _id_ or _name_ attributes the Dom Clobbering protection can be configured to prepend the namespace 'user-content-' (Similar to DomPurify SANITIZE_NAMED_PROPS)

```javascript
const sanitizerOptions = {
clobberingProtection: {
document: true,
form: false, //Set true for FormRender
namespaceAttributes: true,
},
backendOrder: ['dompurify','sanitizer','fallback'],
};
$(container).formBuilder(options);
```
5 changes: 3 additions & 2 deletions docs/formBuilder/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Key files / folders:
* `form-render.js` - the library & code for rendering formData json/xml created by formbuilder
* `helper.js` - reusable methods that are used throughout `form-builder.js`
* `layout.js` - the layout engine that produces each row of the form, and determines how the label, help text, and control widget will each fit together.
* `sanitizer.js` - Script injection and DOM clobbering protection library for formbuilder
* `utils.js` - resuable methods thare are used in both `form-builder.js` and `form-render.js`

# Controls
Expand All @@ -17,9 +18,9 @@ Key files / folders:

Each control is represented by a class which inherits from the `control` class defined in `control.js`. A control class may be used by multiple types of controls.

For an example in of how to [**create a new control**](controls), check out the Readme.md in the `control/` directory.
For an example in of how to [**create a new control**](controls.md), check out the Readme.md in the `control/` directory.

For an example in of how to [**create a new control plugin**](control-plugins), check out the Readme.md in the `control/` directory.
For an example in of how to [**create a new control plugin**](control-plugins.md), check out the Readme.md in the `control/` directory.

The parent class defined in `control.js` has two types of methods:
* object level methods which are used to manipulate and create an instance of that control on a form
Expand Down
9 changes: 8 additions & 1 deletion docs/formRender/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ var defaults = {
warning: function(message) {
return console.warn(message);
}
}
},
sanitizerOptions: {
clobberingProtection: {
document: false,
form: false,
},
backendOrder: ['dompurify','sanitizer','fallback'],
},
}
</code></pre>
4 changes: 4 additions & 0 deletions docs/formRender/options/sanitizerOptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SanitizerOptions
`sanitizerOptions` provides the configuration of the built-in script injection, DOM Clobbering and Form hijacking protection.

Documentation is provided for this option in the FormBuilder [sanitizeOptions](../../formBuilder/options/sanitizerOptions.md) page
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ nav:
- formBuilder Demos:
- AngularJS: formBuilder/demos/angular.md
- Basic: formBuilder/demos/basic.md
- Bootstrap Grid: formBuilder/demos/bootstrap-grid.md
- Custom Elements: formBuilder/options/templates.md
- Data: formBuilder/demos/data.md
- React: formBuilder/demos/react.md
Expand Down Expand Up @@ -76,6 +77,7 @@ nav:
- persistDefaultFields: formBuilder/options/persistDefaultFields.md
- replaceFields: formBuilder/options/replaceFields.md
- roles: formBuilder/options/roles.md
- sanitizerOptions: formBuilder/options/sanitizerOptions.md
- scrollToFieldOnAdd: formBuilder/options/scrollToFieldOnAdd.md
- showActionButtons: formBuilder/options/showActionButtons.md
- sortableControls: formBuilder/options/sortableControls.md
Expand All @@ -98,10 +100,12 @@ nav:
- formRender Options:
- Overview: formRender/options.md
- container: formRender/options/container.md
- disableHTMLLabels: formRender/options/disableHTMLLabels.md
- formData: formRender/options/formData.md
- layout: formRender/options/layout.md
- layoutTemplates: formRender/options/layoutTemplates.md
- render: formRender/options/render.md
- sanitizerOptions: formRender/options/sanitizerOptions.md
- Contributing: contributing.md
- License: license.md
extra_css:
Expand Down
7 changes: 7 additions & 0 deletions src/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export const defaultOptions = {
roles: {
1: 'Administrator',
},
sanitizerOptions: {
clobberingProtection: {
document: false,
form: false,
},
backendOrder: [], //'dompurify','sanitizer','fallback'
},
scrollToFieldOnAdd: true,
showActionButtons: true,
sortableControls: false,
Expand Down
32 changes: 20 additions & 12 deletions src/js/form-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getContentType,
generateSelectorClassNames,
} from './utils'
import { attributeWillClobber, setElementContent, setSanitizerConfig } from './sanitizer'
import fontConfig from '../fonts/config.json'
const css_prefix_text = fontConfig.css_prefix_text

Expand Down Expand Up @@ -65,6 +66,13 @@ function FormBuilder(opts, element, $) {
formBuilder.colWrapperClass = colWrapperClass
formBuilder.fieldSelector = opts.enableEnhancedBootstrapGrid ? rowWrapperClassSelector : defaultFieldSelector

//Initialise HTML sanitizer
setSanitizerConfig(opts.sanitizerOptions)
if ($(element).closest('form').length) {
//Due to Dom Clobbering potential with the stage and the lack of requirement for a Form element, warn for this type of setup
opts.notify.warning('WARNING: FormBuilder does not support being contained with a <form> Element')
}

// prepare a new layout object with appropriate templates
if (!opts.layout) {
opts.layout = layout
Expand Down Expand Up @@ -93,11 +101,7 @@ function FormBuilder(opts, element, $) {
let cloneControls

function enhancedBootstrapEnabled() {
if (!opts.enableEnhancedBootstrapGrid) {
return false
}

return true
return !!opts.enableEnhancedBootstrapGrid
}

$stage.sortable({
Expand Down Expand Up @@ -1710,6 +1714,14 @@ function FormBuilder(opts, element, $) {
$valWrap.toggle(e.target.value !== 'quill')
})

$stage.on('change', '[name="name"]', e => {
const name = e.target.value
if (attributeWillClobber(name)) {
//@TODO Notify the user of this potential issue
opts.notify.error('Potential for Dom Clobbering with field name ' + name)
}
})

const stageOnChangeSelectors = ['.prev-holder input', '.prev-holder select', '.prev-holder textarea']
$stage.on('change', stageOnChangeSelectors.join(', '), e => {
let prevOptions
Expand Down Expand Up @@ -1748,11 +1760,7 @@ function FormBuilder(opts, element, $) {
if (!target.classList.contains('fld-label')) return
const value = target.value || target.innerHTML
const label = closest(target, '.form-field').querySelector('.field-label')
if (config.opts.disableHTMLLabels) {
label.textContent = value
} else {
label.innerHTML = parsedHtml(value)
}
setElementContent(label, parsedHtml(value), config.opts.disableHTMLLabels)
})

// remove error styling when users tries to correct mistake
Expand Down Expand Up @@ -2210,7 +2218,7 @@ function FormBuilder(opts, element, $) {
gridMode = false
gridModeTargetField = null

$(gridModeHelp).html('')
$(gridModeHelp).empty()

//Show controls
$cbUL.css('display', 'unset')
Expand All @@ -2220,7 +2228,7 @@ function FormBuilder(opts, element, $) {

function buildGridModeHelp() {
$(gridModeHelp).html(`
<div style='padding:5px'>
<div style='padding:5px'>
<h3 class="text text-center">Grid Mode</h3>
<table style='border-spacing:7px;border-collapse: separate'>
Expand Down
19 changes: 16 additions & 3 deletions src/js/form-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './control/index'
import controlCustom from './control/custom'
import { defaultI18n } from './config'
import '../sass/form-render.scss'
import { setSanitizerConfig } from './sanitizer'

/**
* FormRender Class
Expand Down Expand Up @@ -38,6 +39,15 @@ class FormRender {
},
onRender: () => {},
render: true,
sanitizerOptions: {
clobberingProtection: {
document: true,
form: false,
namespaceAttributes: true, //clobbered names will be prefixed with user-content-
},
backendOrder: ['dompurify','sanitizer','fallback'],

},
templates: {}, // custom inline defined templates
notify: {
error: error => {
Expand All @@ -54,6 +64,9 @@ class FormRender {
this.options = jQuery.extend(true, defaults, options)
this.instanceContainers = []

//Override any sanitizer configuration
setSanitizerConfig(this.options.sanitizerOptions)

if (!mi18n.current) {
mi18n.init(this.options.i18n)
}
Expand Down Expand Up @@ -134,7 +147,7 @@ class FormRender {
* @param {Number} instanceIndex - instance index
* @return {Object} sanitized field object
*/
santizeField(field, instanceIndex) {
sanitizeField(field, instanceIndex) {
const sanitizedField = Object.assign({}, field)
if (instanceIndex) {
sanitizedField.id = field.id && `${field.id}-${instanceIndex}`
Expand Down Expand Up @@ -191,7 +204,7 @@ class FormRender {
const engine = new opts.layout(opts.layoutTemplates, false, opts.disableHTMLLabels)
for (let i = 0; i < opts.formData.length; i++) {
const fieldData = opts.formData[i]
const sanitizedField = this.santizeField(fieldData, instanceIndex)
const sanitizedField = this.sanitizeField(fieldData, instanceIndex)

// determine the control class for this type, and then process it through the layout engine
const controlClass = control.getClass(fieldData.type, fieldData.subtype)
Expand Down Expand Up @@ -249,7 +262,7 @@ class FormRender {
'To render a single element, please specify a single object of formData for the field in question',
)
}
const sanitizedField = this.santizeField(fieldData)
const sanitizedField = this.sanitizeField(fieldData)

// determine the control class for this type, and then build it
const engine = new opts.layout()
Expand Down
Loading

0 comments on commit 57bf6e6

Please sign in to comment.