运行时反思/内省
这是解决其中一些问题的一种方法,我并不认为这是“最好”的方法(根本),这是一种尝试。
如果可以拦截一些“可利用”的函数和方法,并查看“调用”(每次调用)是否是从生成它的服务器发出的,那么这可能会被证明是有用的,因为我们可以看到该调用是否来自“来自稀薄的空气”(开发工具)。
如果要采用这种方法,那么首先我们需要一个函数来获取call-stack
并丢弃那些不是的FUBU(由我们为我们)。如果这个函数的结果为空,哈扎! - 我们没有打电话,我们可以进行相应的处理。
一两句话
为了使其尽可能简短,下面的代码示例如下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;
};
在畏缩之后,记住这是“即时”编写的“概念证明”,但经过测试并且它有效。根据您的意愿进行编辑。
stak()
- short explanation
- 唯一的2个相关参数是第1个2,其余的是因为..懒惰(简短回答)
- 两个参数都是可选的
- 如果第一个参数
x
是一个数字,那么例如stack(0)
返回日志中的第一项,或者undefined
- 如果第二个参数
a
是一个string
-or an array
那么例如stack(undefined, "anonymous")
allows“匿名”,尽管它被“省略”o
- 其余代码只是快速解析堆栈,这应该适用于基于 webkit 和 gecko 的浏览器(chrome 和 firefox)
- 结果是一个字符串数组,每个字符串都是一个由单个空格分隔的日志条目,如下所示
function file line
- 如果在日志条目(解析之前的文件名的一部分)中找不到域名,那么它不会出现在结果中
- 默认情况下它忽略文件名
/
(完全正确)所以如果你测试这段代码,放入一个单独的.js
文件将产生比在中更好的结果index.html
(通常)-或使用任何网络根机制
- 别担心
_fake_
目前,它位于jack
下面的函数
现在我们需要一些工具
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;
};
烘烤和钻孔 - 概要
这些都是不言自明的,所以,一些简单的例子就足够了
- using
bore
to get一个属性:console.log(bore(window,"XMLHttpRequest.prototype.open"))
- using
bore
to set一个属性:bore(window,"XMLHttpRequest.prototype.open",function(){return "foo"})
- using
bore
to rip(不小心毁掉):bore(window,"XMLHttpRequest.prototype.open",null)
- using
bake
to harden现有财产:bake(XMLHttpRequest.prototype,'open')
- using
bake
to define一个新的(硬)属性: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()
- explanation
- 它需要 2 个参数,第一个为字符串(用于
bore
),第二个用作拦截器(函数)
- 前几条评论解释了一点..“add”行只是将另一个拦截器添加到同一引用
-
jack
废弃现有函数,将其收起,然后使用“拦截器函数”重播参数
- 拦截器可以返回
undefined
或一个值,如果没有返回任何值,则不会调用原始函数
- 拦截器返回的第一个值用作调用原始值的参数,并将结果返回给调用者/调用者
- that
fail(":(")
是故意的;如果您没有该功能,则会抛出错误 - 仅当jack()
failed.
Examples
我们来预防一下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
事件,那么我们的拦截器将无法捕获该事件;但是,我们可以使用突变观察者 https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver并监视“属性变化”。在属性更改(或新节点)时,我们可以检查是否进行了更改FUBU(或不)与我们的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});
结论
这些只是处理严重问题的几种方法;尽管我希望有人发现这很有用,并且请随意编辑这个答案,或者发布更多(或替代/更好)的提高前端安全性的方法。