如何防止脚本注入攻击

问题描述:

这个主题一直是StackOverflow和其他许多技术论坛上许多问题和答案的祸根;但是,其中大多数都是特定于特定条件的,甚至更糟:通过dev-tools-consoledev-tools-elements甚至address-bar进行的脚本注入预防中的全面"安全性被认为不可能"进行保护.该问题旨在解决这些问题,并随着技术的改进而成为当前和历史的参考-或发现新的/更好的方法来解决特定于script-injection攻击的浏览器安全问题.

This topic has been the bane of many questions and answers on StackOverflow -and in many other tech-forums; however, most of them are specific to exact conditions and even worse: "over-all" security in script-injection prevention via dev-tools-console, or dev-tools-elements or even address-bar is said to be "impossible" to protect. This question is to address these issues and serve as current and historical reference as technology improves -or new/better methods are discovered to address browser security issues -specifically related to script-injection attacks.

有很多方法可以实时"提取或操作信息;特别是,无论SSL/TLS为何,截取从输入中收集的信息(要传输到服务器)都是非常容易的.

There are many ways to either extract -or manipulate information "on the fly"; specifically, it's very easy to intercept information gathered from input -to be transmitted to the server - regardless of SSL/TLS.

此处 不管它有多粗略",都可以轻松地使用该原理来制作模板,然后将其复制并粘贴到浏览器控制台中的eval()中,以执行各种令人讨厌的事情,例如:

Have a look here Regardless of how "crude" it is, one can easily use the principle to fabricate a template to just copy+paste into an eval() in the browser console to do all kinds of nasty things such as:

  • console.log()通过 XHR
  • 拦截了传输中的信息
  • 操纵POST-数据,更改用户引用,例如UUIDs
  • 通过检查JS代码,cookiesheaders
  • ,向目标服务器替代项GET(& post)提供请求信息,以中继(或获取)信息.
  • console.log() intercepted information in transit via XHR
  • manipulate POST-data, changing user-references such as UUIDs
  • feed the target-server alternative GET (& post) request information to either relay (or gain) info by inspecting the JS-code, cookies and headers

这种攻击对于未经训练的人来说似乎是微不足道的,但是当涉及到高度动态的界面时,这很快就成为了 噩梦 -等待被利用

This kind of attack "seems" trivial to the untrained eye, but when highly dynamic interfaces are in concern, then this quickly becomes a nightmare -waiting to be exploited.

我们都知道您不能信任前端" ,并且服务器应负责安全性;但是-我们心爱的访客的隐私/安全又如何呢?许多人使用JavaScript创建快速应用程序",要么都不了解(或不在乎)后端安全性.

We all know "you can't trust the front-end" and the server should be responsible for security; however - what about the privacy/security of our beloved visitors? Many people create "some quick app" in JavaScript and either do not know (or care) about the back-end security.

确保前端和后端的安全对普通攻击者而言将是可怕的,并且还减轻了服务器的负载(在许多情况下).

Securing the front-end as well as the back-end would prove formidable against an average attacker, and also lighten the server-load (in many cases).

Google和Facebook都采用了一些缓解这些问题的方法,并且它们可以工作.因此,这不是不可能"的,但是它们非常针对各自的平台,要实现,需要使用整个框架以及大量工作-仅覆盖基础知识.

Both Google and Facebook have implemented some ways of mitigating these issues, and they work; so it is NOT "impossible", however, they are very specific to their respective platforms and to implement requires the use of entire frameworks plus a lot of work -only to cover the basics.

无论这些保护机制中的某些看起来多么丑陋";目标是要在某种程度上帮助(缓解/预防)安全问题,使攻击者难以承受.众所周知,您不能阻止黑客,只能阻止他们的努力"..

Regardless of how "ugly" some of these protection mechanisms may appear; the goal is to help (mitigate/prevent) security issues to some degree, making it difficult for an attacker. As everybody knows by now: "you cannot keep a hacker out, you can only discourage their efforts".

目标是拥有一套简单的工具(功能):

The goal is to have a simple set of tools (functions):

  • 这些必须使用普通(原始)javascript
  • 它们总共不应超过几行代码(最多200行)
  • 他们必须是immutable,以防止攻击者重新捕获"
  • 这些不得与任何(流行的)JS框架发生冲突,例如React,Angular等
  • 不必漂亮",但至少可读性强,单线"欢迎
  • 跨浏览器兼容,至少具有良好的百分位数
  • these MUST be in plain (vanilla) javascript
  • together they should NOT exceed a few lines of code (at most 200)
  • they have to be immutable, preventing "re-capture" by an attacker
  • these MUST NOT clash with any (popular) JS frameworks, such as React, Angular, etc
  • does NOT have to be "pretty", but readable at least, "one-liners" welcome
  • cross-browser compatible, at least to a good percentile

运行时反思/自省

这是解决其中一些问题的一种方法,我不认为这是最佳"方法(完全),这是一种尝试. 如果可以拦截某些可解释的"函数和方法,并查看调用"(每次调用)是否是从产生该调用的服务器进行的,那么这可以证明是有用的,因为我们可以看到调用是否来自稀疏的空气"(开发工具).

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).

如果要采用这种方法,那么首先我们需要一个捕获call-stack的函数,并丢弃不是 FUBU 的函数(对于我们而言).如果此函数的结果为空,请注意! -我们没有拨打电话,我们可以照此进行.

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.

为了使之简短化&以下代码示例尽可能简单,遵循 DRYKIS 原则,即:

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

  • 不要重复自己,保持简单
  • 更少的代码"欢迎熟练的人
  • 太多的代码和评论"吓跑了所有人
  • 如果您能阅读代码,请使其美观

话虽如此,请原谅我的速记",然后进行解释

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

    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()-简短说明

  • 仅有的2个相关参数是第1个2,其余的是因为..懒惰(简短答案)
  • 两个参数都是可选的
  • 如果第一个参数x是数字,则例如stack(0)返回日志中的第一项,或undefined
  • 如果第二个参数astring-或array,则例如stack(undefined, "anonymous") 允许匿名",即使在o
  • 中被省略"
  • 其余代码只是快速解析堆栈,这在webkit&基于壁虎的浏览器(chrome和firefox)
  • 结果是一个字符串数组,每个字符串都是一个用function file line
  • 分隔的日志条目
  • 如果在登录条目(解析前文件名的一部分)中找不到域名,则该域名将不在结果中
  • 默认情况下,它会忽略文件名/(完全),因此,如果您测试此代码,则放入单独的.js文件比(通常)在index.html中会产生更好的结果-或使用任何Web根机制
  • 暂时不用担心_fake_,它位于下面的jack函数中
  • 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
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()-强化现有对象属性(或定义新属性)的简写

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

  • 使用bore获取一个属性:console.log(bore(window,"XMLHttpRequest.prototype.open"))
  • 使用bore设置属性 :bore(window,"XMLHttpRequest.prototype.open",function(){return "foo"})
  • 使用bore撕毁(粗心大意地破坏):bore(window,"XMLHttpRequest.prototype.open",null)
  • 使用bake强化现有属性:bake(XMLHttpRequest.prototype,'open')
  • 使用bake定义一个新的(硬)属性:bake(XMLHttpRequest.prototype,'bark',function(){return "woof!"})
  • 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!"})

现在,当我们设计一个简单而有效的拦截器时,我们可以利用上述所有优势,这绝不是完美"的,但它已经足够了;解释如下:

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()-说明

  • 它有2个参数,第一个用作字符串(用于bore),第二个用作拦截器(函数)
  • 前几条评论有点解释.."add"行只是将另一个拦截器添加到相同的引用
  • jack放置一个现有函数,将其收藏起来,然后使用拦截器函数"重播参数
  • 拦截器可以返回undefined或一个值,如果没有返回任何值,则不调用原始函数
  • 拦截器返回的第一个值用作调用原始文件的参数,并将结果返回给调用者/调用者
  • fail(":(")是有意的;如果您没有该功能,则会抛出错误-仅在jack()失败的情况下.
  • 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.
    • 让我们阻止eval在控制台-或地址栏中使用

      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?")});

可扩展性

如果您想通过 DRY-er jack进行交互,则将进行以下测试,并且效果很好:

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!")});

然后,聪明的攻击者可能会使用 Elements (开发工具)来修改某些元素的属性,并为其提供一些onclick事件,然后我们的拦截器赢得了胜利不明白这一点;但是,我们可以使用 mutation-observer ,监视属性更改".在属性更改(或新节点)更改后,我们可以通过stak()检查来检查是否进行了 FUBU 更改:

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.