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}
{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') }
];

View file

@ -137,14 +137,34 @@ async function listDTAjax(context, listId, params) {
builder => builder
.from('custom_fields')
.innerJoin('lists', 'custom_fields.list', 'lists.id')
// This self join is to provide 'option' fields a reference to their parent grouped field. If the field is not an option, it refers to itself
// All this is to show options always below their group parent
.innerJoin('custom_fields AS parent_fields', function() {
this.on(function() {
this.on('custom_fields.type', '=', knex.raw('?', ['option']))
.on('custom_fields.group', '=', 'parent_fields.id');
}).orOn(function() {
this.on('custom_fields.type', '<>', knex.raw('?', ['option']))
.on('custom_fields.id', '=', 'parent_fields.id');
});
})
.where('custom_fields.list', listId),
[ 'custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list' ],
{
orderByBuilder: (builder, orderColumn, orderDir) => {
// We use here parent_fields to keep options always below their parent group
if (orderColumn === 'custom_fields.order_list') {
builder.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc'); // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
builder
.orderBy(knex.raw('-parent_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
.orderBy('parent_fields.name', orderDir)
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc')
} else {
builder.orderBy(orderColumn, orderDir);
const parentColumn = orderColumn.replace(/^custom_fields/, 'parent_fields');
builder
.orderBy(parentColumn, orderDir)
.orderBy('parent_fields.name', orderDir)
.orderBy(knex.raw('custom_fields.type = "option"'), 'asc');
}
}
}
@ -165,9 +185,13 @@ async function listGroupedDTAjax(context, listId, params) {
{
orderByBuilder: (builder, orderColumn, orderDir) => {
if (orderColumn === 'custom_fields.order_list') {
builder.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc'); // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
builder
.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc') // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
.orderBy('custom_fields.name', orderDir);
} else {
builder.orderBy(orderColumn, orderDir);
builder
.orderBy(orderColumn, orderDir)
.orderBy('custom_fields.name', orderDir);
}
}
}
@ -203,6 +227,8 @@ async function serverValidate(context, listId, data) {
async function _validateAndPreprocess(tx, listId, entity, isCreate) {
enforce(entity.type === 'option' || !entity.group, 'Only option may have a group assigned');
enforce(entity.type !== 'option' || entity.group, 'Option must have a group assigned.');
enforce(entity.type !== 'option' || (entity.orderListBefore === 'none' && entity.orderSubscribeBefore === 'none' && entity.orderManageBefore === 'none'), 'Option cannot be made visible');
enforce(!entity.group || await tx('custom_fields').where({list: listId, id: entity.group}).first(), 'Group field does not exist');
enforce(entity.name, 'Name must be present');
@ -298,6 +324,7 @@ async function create(context, listId, entity) {
const ids = await tx('custom_fields').insert(filteredEntity);
id = ids[0];
await _sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
if (columnName) {