-
Notifications
You must be signed in to change notification settings - Fork 423
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
feat: add basic html sanitizer w/regex to avoid xss scripting attack #652
feat: add basic html sanitizer w/regex to avoid xss scripting attack #652
Conversation
@RasmusBergHogia you might be interested in this PR, it should help and it's simple |
@6pac I just finished testing in my 3rd and last lib, seems all good on my side, feel free to merge. That is also the last PR I wanted to do before a new release. Thanks mate |
- to go with previous PR #652 that I added recently and in some circumstance the user might want to provide his own sanitizer.
- to go with previous PR #652 that I added recently and in some circumstance the user might want to provide his own sanitizer.
ffc682b#commitcomment-61225015 This broke prod for me rolled back to 2.4.42. this removes the
evil could theoretical be a script tag ... we need to sanitize the variables we use before injecting them into html. if this borked it for you and you are looking for a fix |
but then I have to look up the dom element when slick grid renders it and add the even and if slick girid destroys it for memory optimization the listeners would get wiped out. I though that was a relitivly clean solution... if anyone knows a nicer way let me know maybe. This is what I am doing if you are curious it let me programmatically add action buttons to a row or any HTML thing. theoretical and get a callback i ran into a few issues trying to use addEventListener I think but dont remember details. this whole thing is in a dynamic jsPanel4 which sometimes messes with listeners too since the whole dom gets creates and destroyed. /**
* We have to do it this way so the formatters have access to 'this' since js class objects are just normal functions :/
*/
initFormatters() {
this.actionFormatter = (row, cell, value, columnDef, dataContext) => {
if (typeof this._actions == "undefined") {
this._actions = [];
}
let html = ``;
this._actions.forEach(item => {
if (typeof item.formatter === 'function') {
html += item.formatter(item, dataContext, this, row, cell, value, columnDef)
} else {
html += item.string.replace('$id', dataContext.id);
}
});
// html += `<button onclick="_globalGrids[${this._id}].deleteByFileId('${dataContext.id}')" class="slick-icon-btn btn btn-danger btn-sm float-right" title="Delete This Version">
// <i class="fa fa-trash" aria-hidden="true"></i>
// </button>`
//
// html += `<button onclick="Files.downloadFile('${dataContext.id}')" class="slick-icon-btn btn btn-primary btn-sm float-right" title="Download This Version">
// <i class="fa fa-download" aria-hidden="true"></i>
// </button>`
// html += `<button onclick="_globalGrids[${this._id}].viewFile('${dataContext.id}', '${dataContext.file_name}')" class="slick-icon-btn btn btn-success btn-sm float-right" title="View This Version">
// <i class="fa fa-eye" aria-hidden="true"></i>
// </button>`
return html;
};
}
/**
* add an action WARNING USE addActionButton or make sure string calls the fn you need
* @param item - a full item or the css selector for the item
* @param string - the html string for the item $id in this string will be replaced with the row.id in the formatter
* @param fn - the fn to be executed on click
*/
addAction(item, string, fn, formatter) {
// this.formatter = formatter;
if (typeof item == "object") {
string = item.string || string;
fn = item.fn || fn;
formatter = item.formatter || formatter;
item = item.selector;
}
if (typeof string == 'undefined') {
throw Error("item.string not provided in object and string is undefined");
}
if (typeof fn == 'undefined') {
throw Error("item.fn not provided in object and fn is undefined");
}
if (typeof item == 'undefined') {
console.warn("No Item");
}
item = {
selector: item,
string: string,
fn: fn,
formatter: formatter
}
if (!this._actions) {//init
this._actions = [];
}
this._actions.push(item);
//this is so that the first few rows that have already rendered get updated with the new action
if (this.grid) {// only if the grid is initialized
this.grid.invalidateAllRows();
this.grid.render();
}
}
updateActionString(itemSelector, string) {
let a = this._actions.find(a => a.selector == itemSelector)
if (!a) {
console.error("No Action found with selector: ", itemSelector);
return;
}
a.string = string;
if (this.grid) {// only if the grid is initialized
this.grid.invalidateAllRows();
this.grid.render();
}
}
/**
* This is convoluted but it works there is probably a better way
* @param index
* @param row_id
* @private
*/
_executeAction(index, row_id) {
this._actions[index].fn(row_id, this);
}
/**
* Add an action to the grid this will show up in the actions column
* this is a utility extension of addAction
* @param name - the name for a selector not really used
* @param title - the tooltip
* @param html - innerHtml for button
* @param fn - fn(row_id, this) called onclick
* @param options - optional options
* @param {string} [options.colorClass='btn-info'] - a class for styling the button ususaly color
* @param {function} [options.formatter] - a function to be called from the slick formatter see initFormatters()
*/
addActionButton(name, title, html, fn, options) {
console.debug("Action: add Action Button: ", name)
options = options || {}
options.colorClass = options.colorClass || 'btn-info'
// options = bs.mergeDeep(defaults, options);
if (!this._actions) this._actions = [];
// `<span name="${name}" title="${title}" class="btn btn-sm btn-outline-dark">${html}</span>`,
let actionIndex = this._actions.length;
let item = {
selector: `button[name=${name}]`,
string: `<button name="${name}" onclick="_globalGrids[${this._id}]._executeAction(${actionIndex},'$id')" class="slick-icon-btn btn btn-sm float-right ${options.colorClass}" title="${title}">
${html}
</button>`,
fn: fn
}
if (typeof options.formatter == 'function') {
item.formatter = options.formatter;
console.warn("Action: FORMATTER: ", item.formatter)
} else {
console.warn("Action: NO FORMATTER: ", options.formatter)
}
this.addAction(item);
}
|
Does that work? how is the event listener getting atached to the dom element slick grid constructs out of the outer html string. //adding this into the addActionButton instead of just a string doesn't work
let btn = document.createElement('button');
btn.name = name;
btn.setAttribute('class', `slick-icon-btn btn btn-sm float-right ${options.colorClass}` );
btn.setAttribute('title', title);
btn.innerHTML = html;
btn.addEventListener("click", e=>{
console.log("WOW THIS WORKS:", e);
this._executeAction(actionIndex,'$id')
})
//... item.string = btn.outerHTML; this doesn't work could it be that the js engine is caching the dome node when its constructed very closely in the formatted? do you have an example this seems like it shouldn't work. ... It seems the intended way is to add data attributes to your elements and then check if they were clicked on in the grid.onClick event... this doesn't let you have en event on input change. A solution might be to add event listeners when a cell gets clicked on? ... jquery events might magically work too. |
I also went through the same problem you are having, I understand the importance of applying XSS, but I have many deployments using "onclick" in the content of the cells. He also made some adjustments to "disable" this option: For more information I suggest you see this topic: |
As I wrote on another post, I had another user saying the same thing, but the thing is that you're never supposed to put JS code in a formatter because that is an open door to XSS scripting attack, the correct and more secure way of dealing with events is the use of the SlickGrid exported events, for example the onClick SlickGrid event (you also get all the On another note, I did another PR #657 to optionally override the sanitizer to avoid having too much breaking, which is merged, however we didn't release a new SlickGrid version yet. The risk with JS code in Formatters is that a user with bad intention could take advantage of scripting attack by saving cell data that include bad JS code directly in the cell data that could end up being used because you have JS code in your formatter. Note that I never tested that technique but my main reason of creating the sanitizer PR was to prevent such attack (which I have never tested). in my mind it's better to play safe than say "sorry you got hacked..." |
Since I see "Action" throughout your post, so perhaps you can use the Cell Menu plugin that I created which was created for that exact reason of having a way to create a nice popup menu with few different actions/commands. |
never is a strong word in this case the only xss would be a malicious id. from the database so it should be safeish. I wonder if there is a way for slick grid to expose this functionality. the command api is almost there SlickGrid/plugins/slick.contextmenu.js Line 58 in 416992e
that is probably good enough thanks for that example I wish I saw it sooner :) but somthing like this might be nice:
then in my code actions are events and just need to be updated in the columDef when someone adds an action. this could possibly be done by in the sanitize function replacing thanks for all the info lmk what you thing might spend some time on rewriting it later. |
yeah maybe that word is too strong, a better word may be "it's preferable to use SlickGrid events"
I'm not sure what you mean by that code, where would that go and what would it do? I'm not sure if it's related or not (I think it is) but what I've done in my lib is to wrap the grid this.columnDefinitions = [{
id: 'firstName', /*....*/
onCellClick: (event, args) => {
const row, cell, dataView, columnDef, dataContext = args;
}
}]; As for the sanitizer, you'll be able to able to override it so that should help. We just need a new release but I'd like to have another open PR to get through before doing a new release. |
@ghiscoding let me know when you're ready for a release |
Thanks to @6pac this is now released with version this.gridOptions = {
sanitizer: function (dirtyHtml) {
return dirtyHtml; // return the same input string without sanitizing it
// or try a different sanitizer that will let `onX` events get through but block any other JavaScript code
// something like this maybe:
// return dirtyHtml.replace(/javascript:([^>]*)[^>]*|(<\s*)(\/*)script([<>]*).*(<\s*)(\/*)script(>*)|(<)(\/*)(script|script defer)(.*)(>|>">)/gi, '');
},
// ...
} I still strongly suggest to eventually think about rewriting your code and use the more appropriate way of using SlickGrid events (like Happy Coding 👨💻 |
@ghiscoding a heads up that there is also a new example 'example-drag-row-between-grids' that features a new row drag plugin designed to drag rows from one grid to another. I added this one directly to source in my repo. It's 98% the same as the other row drag plugin, just a few tweaks. |
innerHTML
for example$el.innerHTML = '<div><script>alert('XSS')</script></div>
TODOs
For example, see below for what it covers and what gets replaced/removed (the top portion shows some common xss attack & the bottom portion shows the cleaner string after regex string replacement)