Options always shown below the group no matter how the list is sorted

XSS protection for tables and trees
This commit is contained in:
Tomas Bures 2017-08-13 11:32:31 +02:00
parent e230510b72
commit d9211377dd
6 changed files with 96 additions and 21 deletions

View file

@ -239,8 +239,21 @@ class Table extends Component {
type: 'html',
createdCell: createdCellFn
});
}
// FIXME, sift all columns through renderToStaticMarkup in order to sanitize the HTML
// XSS protection
for (const column of columns) {
const originalRender = column.render;
column.render = (data, ...rest) => {
if (originalRender) {
const markup = originalRender(data, ...rest);
return ReactDOMServer.renderToStaticMarkup(<div>{markup}</div>);
} else {
return ReactDOMServer.renderToStaticMarkup(<div>{data}</div>)
}
};
column.title = ReactDOMServer.renderToStaticMarkup(<div>{column.title}</div>);
}
const dtOptions = {

View file

@ -86,6 +86,17 @@ class TreeTable extends Component {
return this.props.selection !== nextProps.selection || this.state.treeData != nextState.treeData;
}
// XSS protection
sanitizeTreeData(unsafeData) {
const data = unsafeData.slice();
for (const entry of data) {
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>)
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>)
entry.children = this.sanitizeTreeData(entry.children);
}
return data;
}
componentDidMount() {
if (!this.props.data && this.props.dataUrl) {
this.loadData(this.props.dataUrl);
@ -109,10 +120,8 @@ class TreeTable extends Component {
let tdIdx = 1;
// FIXME, sift title through renderToStaticMarkup in order to sanitize the HTML
if (this.props.withDescription) {
const descHtml = ReactDOMServer.renderToStaticMarkup(<div>{node.data.description}</div>);
const descHtml = node.data.description; // This was already sanitized in sanitizeTreeData when the data was loaded
tdList.eq(tdIdx).html(descHtml);
tdIdx += 1;
}
@ -142,7 +151,7 @@ class TreeTable extends Component {
icon: false,
autoScroll: true,
scrollParent: jQuery(this.domTableContainer),
source: this.state.treeData,
source: this.sanitizeTreeData(this.state.treeData),
table: {
nodeColumnIdx: 0
},
@ -156,7 +165,7 @@ class TreeTable extends Component {
}
componentDidUpdate() {
this.tree.reload(this.state.treeData);
this.tree.reload(this.sanitizeTreeData(this.state.treeData));
this.updateSelection();
}

View file

@ -71,7 +71,7 @@ export default class List extends Component {
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('ID'), render: data => `<code>${data}</code>` },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Subscribers') },
{ data: 4, title: t('Description') },
{ data: 5, title: t('Namespace') }

View file

@ -67,7 +67,7 @@ export default class CUD extends Component {
const getOrderOptions = fld => {
return [
{key: 'none', label: t('Not visible')},
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})),
...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
{key: 'end', label: t('End of list')}
];
};
@ -82,6 +82,8 @@ export default class CUD extends Component {
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.settings = data.settings || {};
if (data.default_value === null) {
data.default_value = '';
}
@ -113,6 +115,10 @@ export default class CUD extends Component {
data.dateFormat = data.settings.dateFormat;
break;
}
data.orderListBefore = data.orderListBefore.toString();
data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
data.orderManageBefore = data.orderManageBefore.toString();
});
} else {
@ -177,9 +183,17 @@ export default class CUD extends Component {
state.setIn(['default_value', 'error'], null);
}
let enumOptionErrors;
if ((type === 'radio-enum' || type === 'dropdown-enum') && (enumOptionErrors = this.parseEnumOptions(state.getIn(['enumOptions', 'value'])).errors)) {
state.setIn(['enumOptions', 'error'], <div>{enumOptionErrors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
if (type === 'radio-enum' || type === 'dropdown-enum') {
const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
if (enumOptions.errors) {
state.setIn(['enumOptions', 'error'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} else {
state.setIn(['enumOptions', 'error'], null);
if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
}
}
} else {
state.setIn(['enumOptions', 'error'], null);
}
@ -271,6 +285,14 @@ export default class CUD extends Component {
delete data.renderTemplate;
delete data.enumOptions;
delete data.dateFormat;
if (data.type === 'option') {
data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
} else {
data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
}
});
if (submitSuccessful) {
@ -445,11 +467,13 @@ export default class CUD extends Component {
{fieldSettings}
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>

View file

@ -34,7 +34,9 @@ export default class List extends Component {
const columns = [
{ data: 4, title: "#" },
{ data: 1, title: t('Name') },
{ data: 1, title: t('Name'),
render: (data, cmd, rowData) => rowData[2] === 'option' ? <span><span className="glyphicon glyphicon-record" aria-hidden="true"></span> {data}</span> : data
},
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }
];