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.
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.
intercept exampleHave 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()
intercepted information in transit via XHR
POST
-data, changing user-references such as UUIDs
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.
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).
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):
immutable
, preventing "re-capture" by an attackerThis 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.
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()
- short explanation
x
is a number then e.g. stack(0)
returns the 1st item in the log, or undefined
a
is either a string
-or an array
then e.g. stack(undefined, "anonymous")
allows "anonymous" even though it was "omitted" in o
function file line
/
(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_fake_
for now, it's in the jack
function belowbore()
- 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;
};
These are failry self-explanatory, so, some quick examples should suffice
bore
to get a property: console.log(bore(window,"XMLHttpRequest.prototype.open"))
bore
to set a property: bore(window,"XMLHttpRequest.prototype.open",function(){return "foo"})
bore
to rip (destroy carelessly): bore(window,"XMLHttpRequest.prototype.open",null)
bake
to harden an existing property: bake(XMLHttpRequest.prototype,'open')
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()
- explanation
bore
), the second is used as interceptor (function)jack
deposes an existing function, stows it away, then use "interceptor-functions" to replay argumentsundefined
or a value, if no value is returned from any, the original function is not calledfail(":(")
is intentional; an error will be thrown if you don't have that function - only if the jack()
failed.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?")});
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});
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With