Skip to content
Open
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# @formio/core

## 2.8.1

### Patch Changes

- 5fa2592: FIO-11314: upgrade dompurify to 3.4.12
- dc60a11: FIO-11489 prevent HTML injection via logic

## 2.8.1-api99.0

### Patch Changes

- 5fa2592: FIO-11314: upgrade dompurify to 3.4.12
- dc60a11: FIO-11489 prevent HTML injection via logic

## 2.8.0

### Minor Changes
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formio/core",
"version": "2.8.0",
"version": "2.8.1",
"description": "The core Form.io renderering framework.",
"main": "lib/index.js",
"exports": {
Expand Down Expand Up @@ -110,7 +110,7 @@
"browser-cookies": "^1.2.0",
"core-js": "^3.39.0",
"dayjs": "^1.11.12",
"dompurify": "^3.4.0",
"dompurify": "^3.4.12",
"eventemitter3": "^5.0.0",
"fast-json-patch": "^3.1.1",
"fetch-ponyfill": "^7.1.0",
Expand Down
37 changes: 37 additions & 0 deletions src/utils/__tests__/logic-merge-escape.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from 'chai';
import { setMergeComponentSchema } from '../logic';
import type { LogicContext } from 'types/process/logic/LogicContext';
import type { LogicActionMergeComponentSchema } from 'types/AdvancedLogic';

describe('setMergeComponentSchema', () => {
it('HTML-escapes submission data used when merging schema so labels cannot inject HTML', () => {
const payload = '<img src=x onerror=alert(1)>';
const component: any = {
key: 'checkbox',
type: 'checkbox',
label: 'Original',
};
const data: Record<string, unknown> = {
textField: payload,
};
const scope = {} as LogicContext['scope'];
const context = {
component,
data,
row: data,
path: 'checkbox',
scope,
} as LogicContext;

const action: LogicActionMergeComponentSchema = {
type: 'mergeComponentSchema',
schemaDefinition: `schema = { label: data.textField };`,
};

setMergeComponentSchema(context, action);

expect(component.label).to.not.include('<img');
expect(String(component.label)).to.include('&lt;');
expect(String(component.label)).to.not.equal(payload);
});
});
38 changes: 29 additions & 9 deletions src/utils/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
LogicActionPropertyString,
LogicActionValue,
} from 'types/AdvancedLogic';
import { get, set, clone, isEqual, assign, unset } from 'lodash';
import { evaluate, interpolate } from 'utils/utils';
import { escape, get, set, clone, isEqual, assign, unset } from 'lodash';
import { evaluate, escapeInterpolationDataStrings, interpolate } from 'utils/utils';
import { setComponentScope } from 'utils/formUtil';

export const hasLogic = (context: LogicContext): boolean => {
Expand Down Expand Up @@ -107,15 +107,25 @@ export function setActionStringProperty(
context: LogicContext,
action: LogicActionPropertyString,
): boolean {
const { component } = context;
const { component, data, row, result } = context;
const property = action.property.value;
const textValue = action.property.component
? (action as any)[action.property.component]
: action.text;
const currentValue = get(component, property, '');
const newValue = interpolate(textValue, { ...context, value: '' }, (evalContext: any) => {
evalContext.value = currentValue;
});
const newValue = interpolate(
textValue,
{
...context,
value: '',
data: escapeInterpolationDataStrings(data),
row: escapeInterpolationDataStrings(row),
result: typeof result === 'string' ? escape(result) : escapeInterpolationDataStrings(result),
},
(evalContext: any) => {
evalContext.value = currentValue;
},
);
if (newValue !== currentValue) {
set(component, property, newValue);
return true;
Expand Down Expand Up @@ -158,16 +168,26 @@ export function setMergeComponentSchema(
context: LogicContext,
action: LogicActionMergeComponentSchema,
) {
const { component, data, path } = context;
const { component, data, path, row, result } = context;
const oldValue = get(data, path);
const safeData = escapeInterpolationDataStrings(data);
const safeRow = escapeInterpolationDataStrings(row);
const safeResult =
typeof result === 'string' ? escape(result) : escapeInterpolationDataStrings(result);
const schema = evaluate(
action.schemaDefinition,
{ ...context, value: {} },
{
...context,
value: {},
data: safeData,
row: safeRow,
result: safeResult,
},
'schema',
false,
(evalContext: any) => {
evalContext.value = clone(oldValue);
evalContext.result = context.result;
evalContext.result = safeResult;
},
);
const merged = assign({}, component, schema);
Expand Down
22 changes: 21 additions & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isBoolean, isEmpty, isObject, isPlainObject, isString } from 'lodash';
import { escape, isBoolean, isEmpty, isObject, isPlainObject, isString, mapValues } from 'lodash';
import { EvaluatorOptions, Evaluator } from './Evaluator';
import { isComponentNestedDataType, normalizeContext } from './formUtil';
import {
Expand Down Expand Up @@ -53,6 +53,26 @@ export function unescapeHTML(str: string) {
return doc.documentElement.textContent;
}

/**
* Recursively HTML-escape strings in submission-like objects so logic templates
* (e.g. dynamic labels) cannot treat user-entered values as HTML.
*/
export function escapeInterpolationDataStrings(obj: any): any {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
return escape(obj);
}
if (Array.isArray(obj)) {
return obj.map(escapeInterpolationDataStrings);
}
if (isPlainObject(obj)) {
return mapValues(obj, escapeInterpolationDataStrings);
}
return obj;
}

export function attachResourceToDom(options: ResourceToDomOptions) {
const { name, formio, onload, rootElement, onerror } = options;
let { src } = options;
Expand Down