如何防止脚本注入攻击



简介

这个话题一直是StackOverflow和许多其他技术论坛上许多问答的祸根;然而,它们中的大多数都是特定于特定条件的,甚至更糟:通过dev-tools-consoledev-tools-elements甚至address-bar的脚本注入预防中的"总体"安全性被认为是"不可能"保护的。这个问题是为了解决这些问题,并作为当前和历史的参考,因为技术的改进——或者发现了新的/更好的方法来解决浏览器安全问题——特别是与script-injection攻击有关。

关注点

有很多方法可以"动态"提取或操纵信息;具体来说,无论SSL/TLS如何,都可以很容易地截获从输入中收集的信息,并将其传输到服务器。

截距示例看看这里不管它有多"粗糙",人们都可以很容易地使用制作模板的原理,只需在浏览器控制台中复制+粘贴到eval()中,就可以做各种讨厌的事情,例如:

  • console.log()截获了通过XHR传输的信息
  • 操作POST数据,更改用户引用(如UUIDs)
  • 通过检查JS代码cookiesheaders,将目标服务器备选GET(&post)请求信息提供给中继(或获得)信息

这种攻击在未经训练的人看来"似乎"微不足道,但当高度动态的接口受到关注时,这很快就会成为的噩梦,等待被利用。

我们都知道"你不能信任前端">,服务器应该负责安全;然而,我们心爱的访客的隐私/安全又如何呢?许多人用JavaScript创建"一些快速应用程序",要么不知道(要么关心)后端安全性。

事实证明,保护前端和后端的安全对于普通攻击者来说是非常强大的,同时也减轻了服务器负载(在许多情况下)。

努力

谷歌和脸书都采取了一些缓解这些问题的方法,而且行之有效;因此,这并非"不可能",然而,它们非常具体地针对各自的平台,实现需要使用整个框架加上大量工作——只涉及基础知识。

不管其中一些保护机制看起来多么"丑陋";目标是在一定程度上帮助(减轻/防止)安全问题,使攻击者难以应对。现在大家都知道:"你不能阻止黑客,你只能阻止他们的努力"。

工具&要求

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

  • 这些必须是纯(香草)javascript
  • 它们加在一起不应超过几行代码(最多200行)
  • 它们必须是immutable,以防止攻击者"重新捕获">
  • 这些不能与任何(流行的)JS框架相冲突,例如React、Angular等
  • 不一定要"漂亮",但至少可读性强,欢迎使用"一句话">
  • 跨浏览器兼容,至少达到良好的百分比

运行时反射/反思

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

如果要采用这种方法,那么首先我们需要一个函数来获取call-stack并丢弃不是FUBU的函数(对于我们来说)。如果此函数的结果为空,hazaa!-我们没有打电话,我们可以据此进行。

一两个字

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

  • 不要重复自己,保持简单
  • "少代码"欢迎高手
  • "太多的代码和评论"吓跑了所有人
  • 如果你能读懂代码,那就去做吧

话虽如此,请原谅我的"短兵相接",会给出解释

首先我们需要一些常量和我们的堆栈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;
};

在畏缩之后,我们清楚地意识到,这是作为"概念证明"在飞行中"写的,但经过了测试,它确实有效。边走边编辑。

stak()-简短说明
  • 仅有的两个相关参数是第一个2,其余的是因为。。懒惰(简短回答)
  • 两个参数都是可选的
  • 如果第一个参数x是一个数字,则例如stack(0)返回日志中的第一个项目,或undefined
  • 如果第二个参数astring-或array,则例如stack(undefined, "anonymous")允许"匿名",即使它在o中被"省略">
  • 其余的代码只是快速地解析堆栈,这应该在webkit&基于gecko的浏览器(chrome&firefox)
  • 结果是一个字符串数组,每个字符串是一个日志条目,用一个空格分隔为function file line
  • 如果在日志条目中找不到域名(解析前是文件名的一部分),那么它就不会出现在结果中
  • 默认情况下,它会忽略文件名/(确切地说),因此如果测试此代码,放入一个单独的.js文件将比index.html(通常)或使用任何web根机制产生更好的结果
  • 现在不要担心_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
};
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;
};

烘焙&钻孔-下降

这些都是不言自明的失败,因此,一些快速的例子应该足以满足

  • 使用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({});
jack()-说明
  • 它有两个参数,第一个作为字符串(用于bore),第二个用作拦截器(函数)
  • 前几条评论说明了一些问题。。"add"行只是将另一个拦截器添加到同一引用中
  • jack废弃现有函数,将其收起,然后使用"拦截器函数"来重播参数
  • 拦截器可以返回undefined或一个值,如果任何值都没有返回,则不调用原始函数
  • 拦截器返回的第一个值用作调用原始值的参数,并返回给调用者/调用者的结果
  • CCD_ 45是故意的;如果您没有该函数,则会抛出一个错误——只有在jack()失败的情况下

示例

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

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

可扩展性

如果您想要一种DRY-er的方式与jack接口,则以下内容经过测试并运行良好:

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

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

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

然后,聪明的攻击者可能会使用Elements(开发工具)修改某个元素的属性,给它一些onclick事件,然后我们的拦截器不会捕捉到它;然而,我们可以使用突变观察器来监视"属性变化"。属性更改(或新节点)后,我们可以通过stak()检查检查是否进行了更改FUBU(或未进行更改):

const watchDog=(new MutationObserver(function(l)
{
if(!stak(0)){alert("you again! :D");return};
}));
watchDog.observe(document.documentElement,{childList:true,subtree:true,attributes:true});

结论

这些只是处理一个坏问题的几种方法;尽管我希望有人觉得这很有用,请随意编辑这个答案,或者发布更多(或替代/更好)改进前端安全的方法。

相关内容

  • 没有找到相关文章

最新更新