如何防止脚本注入攻击

arg*_*gon 5 javascript websecurity

介绍

这个话题一直是StackOverflow和其他许多技术论坛上许多问题和答案的祸根。然而,他们大多是针对具体情况更糟糕:“在所有”脚本注射预防通过安全dev-tools-console,或者dev-tools-elements甚至address-bar被说成是“不可能”来保护。这个问题是为了解决这些问题,并随着技术的改进(或发现新的/更好的方法来解决与script-injection攻击有关的浏览器安全性问题),作为当前和历史的参考。

顾虑

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

截取示例

在这里看看 不管它有多“粗鲁”,都可以轻松地使用该原理来制作模板,然后将其复制并粘贴到eval()浏览器控制台中,以进行各种讨厌的事情,例如:

  • console.log()通过XHR拦截传输中的信息
  • 操作POST-data,更改用户引用,例如UUIDs
  • GET通过检查JS代码将目标服务器替代(&张贴)请求信息提供给中继(或获取)信息,cookies以及headers

对于未经训练的人来说,这种攻击“似乎”微不足道,但是当高度动态的界面受到关注时,这很快就变成了一场噩梦-等待被利用。

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

确保前端和后端的安全对普通攻击者来说是非常艰巨的,而且减轻了服务器负载(在许多情况下)。

努力

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

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

工具与要求

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

  • 这些必须使用普通(香草)javascript
  • 在一起,它们不应超过几行代码(最多200行)
  • 必须如此immutable,以防止攻击者“重新捕获”
  • 这些绝不能与任何(流行的)JS框架发生冲突,例如React,Angular等
  • 不一定非要“漂亮”,但至少要可读,欢迎“单线”
  • 跨浏览器兼容,至少具有良好的百分位数

arg*_*gon 3

运行时反思/内省

这是解决其中一些问题的一种方法,我并不认为这是“最好”的方法(根本),这是一种尝试。如果可以拦截一些“可利用”的函数和方法,并查看“调用”(每次调用)是否是从生成它的服务器发出的,那么这可能会被证明是有用的,因为我们可以看到该调用是否来自“来自稀薄的空气”(开发工具)。

如果要采用这种方法,那么首先我们需要一个函数来抓取并丢弃不属于FUBU 的call-stack内容(对于我们来说)。如果这个函数的结果为空,哈扎!- 我们没有打电话,我们可以进行相应的处理。

一两句话

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

  • 不要重复自己,保持简单
  • “少代码”迎高手
  • “太多的代码和注释”吓跑了所有人
  • 如果你能读懂代码 - 继续让它变得漂亮

话虽如此,请原谅我的“速记”,解释如下

首先我们需要一些常量和堆栈获取器

    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;
    };
Run Code Online (Sandbox Code Playgroud)

在畏缩之后,记住这是“即时”编写的“概念证明”,但经过测试并且它有效。根据您的意愿进行编辑。

stak()- 简短的解释
  • 唯一的2个相关参数是第1个2,其余的是因为..懒惰(简短的回答)
  • 两个参数都是可选的
  • 如果第一个参数x是数字,则stack(0)返回日志中的第一项,或者undefined
  • 如果第二个参数a是 -string或 anarray那么 例如stack(undefined, "anonymous") 允许“匿名”,即使它在中被“省略”o
  • 其余代码只是快速解析堆栈,这应该适用于基于 webkit 和 gecko 的浏览器(chrome 和 firefox)
  • 结果是一个字符串数组,每个字符串都是一个由单个空格分隔的日志条目,如下所示function file line
  • 如果在日志条目(解析之前的文件名的一部分)中找不到域名,那么它不会出现在结果中
  • 默认情况下,它会/(完全)忽略文件名,因此如果您测试此代码,放入单独的文件将比(通常)或使用任何 Web 根机制.js产生更好的结果index.html
  • _fake_现在不用担心,它在jack下面的函数中

现在我们需要一些工具

bore()- 通过字符串引用获取/设置/删除对象的某些值
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
};
Run Code Online (Sandbox Code Playgroud) bake()- 强化现有对象属性(或定义新属性)的简写
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;
};
Run Code Online (Sandbox Code Playgroud)

烘烤和钻孔 - 概要

这些是不言自明的,所以,一些简单的例子就足够了

  • 用于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!"})

拦截函数和构造

现在我们可以利用以上所有内容来设计一个简单而有效的拦截器,它绝不是“完美”的,但它应该足够了;解释如下:

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({});
Run Code Online (Sandbox Code Playgroud) jack()- 解释
  • 它需要 2 个参数,第一个作为字符串(用于bore),第二个用作拦截器(函数)
  • 前几条评论解释了一点..“add”行只是将另一个拦截器添加到同一引用
  • jack废弃现有函数,将其收起,然后使用“拦截器函数”重播参数
  • 拦截器可以返回undefined一个值,如果没有返回任何值,则不会调用原始函数
  • 拦截器返回的第一个值用作调用原始值的参数,并将结果返回给调用者/调用者
  • fail(":(")是故意的;如果您没有该功能,则会抛出错误 - 仅当失败时jack()

例子

让我们防止eval在控制台或地址栏中使用

jack("eval",function(a){if(stak(0)){return a}; alert("having fun?")});
Run Code Online (Sandbox Code Playgroud)

可扩展性

如果您想要一种DRY-er方式与 交互jack,以下内容经过测试并且效果良好:

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

现在你可以批量拦截,如下所示:

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

然后,聪明的攻击者可能会使用元素(开发工具)来修改某些元素的属性,为其提供一些onclick事件,然后我们的拦截器将无法捕获该事件;然而,我们可以使用突变观察者并通过该观察者来监视“属性变化”。在属性更改(或新节点)时,我们可以通过检查检查是否进行了FUBU(或没有)更改stak()

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

watchDog.observe(document.documentElement,{childList:true,subtree:true,attributes:true});
Run Code Online (Sandbox Code Playgroud)

结论

这些只是处理严重问题的几种方法;尽管我希望有人发现这很有用,并且请随意编辑这个答案,或者发布更多(或替代/更好)的提高前端安全性的方法。