How to prevent script injection attacks

喜欢而已 提交于 2019-12-04 07:08:39

Runtime Reflection / Introspection

This is a way to address some of these concerns, and I don't claim it's "the best" way (at all), it's an attempt. If one could intercept some "exploitable" functions and methods and see if "the call" (per call) was made from the server that spawned it, or not, then this could prove useful as then we can see if the call came "from thin air" (dev-tools).

If this approach is to be taken, then first we need a function that grabs the call-stack and discard that which is not FUBU (for us by us). If the result of this function is empty, hazaa! - we did not make the call and we can proceed accordingly.

a word or two

In order to make this as short & simple as possible, the following code examples follow DRYKIS principles, which are:

  • don't repeat yourself, keep it simple
  • "less code" welcomes the adept
  • "too much code & comments" scare away everybody
  • if you can read code - go ahead and make it pretty

With that said, pardon my "short-hand", explanation will follow

first we need some constants and our stack-getter

    const MAIN = window;
    const VOID = (function(){}()); // paranoid
    const HOST = `https://${location.host}`; // if not `https` then ... ?

    const stak = function(x,a, e,s,r,h,o)
    {
        a=(a||''); e=(new Error('.')); s=e.stack.split('\n'); s.shift();  r=[]; h=HOSTPURL; o=['_fake_']; s.forEach((i)=>
        {
            if(i.indexOf(h)<0){return}; let p,c,f,l,q; q=1; p=i.trim().split(h); c=p[0].split('@').join('').split('at ').join('').trim();
            c=c.split(' ')[0];if(!c){c='anon'}; o.forEach((y)=>{if(((c.indexOf(y)==0)||(c.indexOf('.'+y)>0))&&(a.indexOf(y)<0)){q=0}}); if(!q){return};
            p=p[1].split(' '); f=p[0]; if(f.indexOf(':')>0){p=f.split(':'); f=p[0]}else{p=p.pop().split(':')}; if(f=='/'){return};
            l=p[1]; r[r.length]=([c,f,l]).join(' ');
        });
        if(!isNaN(x*1)){return r[x]}; return r;
    };

After cringing, bare in mind this was written "on the fly" as "proof of concept", yet tested and it works. Edit as you whish.

stak() - short explanation
  • the only 2 relevant arguments are the 1st 2, the rest is because .. laziness (short answer)
  • both arguments are optional
  • if the 1st arg x is a number then e.g. stack(0) returns the 1st item in the log, or undefined
  • if the 2nd arg a is either a string -or an array then e.g. stack(undefined, "anonymous") allows "anonymous" even though it was "omitted" in o
  • the rest of the code just parses the stack quickly, this should work in both webkit & gecko -based browsers (chrome & firefox)
  • the result is an array of strings, each string is a log-entry separated by a single space as function file line
  • if the domain-name is not found in a log-entry (part of filename before parsing) then it won't be in the result
  • by default it ignores filename / (exactly) so if you test this code, putting in a separate .js file will yield better results than in index.html (typically) -or whichever web-root mechanism is used
  • don't worry about _fake_ for now, it's in the jack function below

now we need some tools

bore() - get/set/rip some value of an object by string reference
const bore = function(o,k,v)
{
    if(((typeof k)!='string')||(k.trim().length<1)){return}; // invalid
    if(v===VOID){return (new Function("a",`return a.${k}`))(o)}; // get
    if(v===null){(new Function("a",`delete a.${k}`))(o); return true}; // rip
    (new Function("a","z",`a.${k}=z`))(o,v); return true; // set
};
bake() - shorthand to harden existing object properties (or define new ones)
const bake = function(o,k,v)
{
    if(!o||!o.hasOwnProperty){return}; if(v==VOID){v=o[k]};
    let c={enumerable:false,configurable:false,writable:false,value:v};
    let r=true; try{Object.defineProperty(o,k,c);}catch(e){r=false};
    return r;
};

bake & bore - rundown

These are failry self-explanatory, so, some quick examples should suffice

  • using bore to get a property: console.log(bore(window,"XMLHttpRequest.prototype.open"))
  • using bore to set a property: bore(window,"XMLHttpRequest.prototype.open",function(){return "foo"})
  • using bore to rip (destroy carelessly): bore(window,"XMLHttpRequest.prototype.open",null)
  • using bake to harden an existing property: bake(XMLHttpRequest.prototype,'open')
  • using bake to define a new (hard) property: bake(XMLHttpRequest.prototype,'bark',function(){return "woof!"})

intercepting functions and constructions

Now we can use all the above to our advantage as we devise a simple yet effective interceptor, by no means "perfect", but it should suffice; explanation follows:

const jack = function(k,v)
{
    if(((typeof k)!='string')||!k.trim()){return}; // invalid reference
    if(!!v&&((typeof v)!='function')){return}; // invalid callback func
    if(!v){return this[k]}; // return existing definition, or undefined
    if(k in this){this[k].list[(this[k].list.length)]=v; return}; //add
    let h,n; h=k.split('.'); n=h.pop(); h=h.join('.'); // name & holder
    this[k]={func:bore(MAIN,k),list:[v]}; // define new callback object

    bore(MAIN,k,null); let f={[`_fake_${k}`]:function()
    {
        let r,j,a,z,q; j='_fake_'; r=stak(0,j); r=(r||'').split(' ')[0];
        if(!r.startsWith(j)&&(r.indexOf(`.${j}`)<0)){fail(`:(`);return};
        r=jack((r.split(j).pop())); a=([].slice.call(arguments));
        for(let p in r.list)
        {
            if(!r.list.hasOwnProperty(p)||q){continue}; let i,x;
            i=r.list[p].toString(); x=(new Function("y",`return {[y]:${i}}[y];`))(j);
            q=x.apply(r,a); if(q==VOID){return}; if(!Array.isArray(q)){q=[q]};
            z=r.func.apply(this,q);
        };
        return z;
    }}[`_fake_${k}`];

    bake(f,'name',`_fake_${k}`); bake((h?bore(MAIN,h):MAIN),n,f);
    try{bore(MAIN,k).prototype=Object.create(this[k].func.prototype)}
    catch(e){};
}.bind({});
jack() - explanation
  • it takes 2 arguments, the first as string (used to bore), the second is used as interceptor (function)
  • the first few comments explain a bit .. the "add" line simply adds another interceptor to the same reference
  • jack deposes an existing function, stows it away, then use "interceptor-functions" to replay arguments
  • the interceptors can either return undefined or a value, if no value is returned from any, the original function is not called
  • the first value returned by an interceptor is used as argument(s) to call the original and return is result to the caller/invoker
  • that fail(":(") is intentional; an error will be thrown if you don't have that function - only if the jack() failed.

Examples

Let's prevent eval from being used in the console -or address-bar

jack("eval",function(a){if(stak(0)){return a}; alert("having fun?")});

extensibility

If you want a DRY-er way to interface with jack, the following is tested and works well:

const hijack = function(l,f)
{
    if(Array.isArray(l)){l.forEach((i)=>{jack(i,f)});return};
};

Now you can intercept in bulk, like this:

hijack(['eval','XMLHttpRequest.prototype.open'],function()
{if(stak(0)){return ([].slice.call(arguments))}; alert("gotcha!")});

A clever attacker may then use the Elements (dev-tool) to modify an attribute of some element, giving it some onclick event, then our interceptor won't catch that; however, we can use a mutation-observer and with that spy on "attribute changes". Upon attribute-change (or new-node) we can check if changes were made FUBU (or not) with our stak() check:

const watchDog=(new MutationObserver(function(l)
{
   if(!stak(0)){alert("you again! :D");return};
}));

watchDog.observe(document.documentElement,{childList:true,subtree:true,attributes:true});

Conclusion

These were but a few ways of dealing with a bad problem; though I hope someone finds this useful, and please feel free to edit this answer, or post more (or alternative/better) ways of improving front-end security.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!