PRIMARY GOAL: Load in external scripts which cannot be read/accessed by other scripts in the SAME context, nor who's loading can be stopped. Special care must be taken that no script can fake the load, inject their own script, or hijack listeners.
Sub-Goal 1 - Add a script element to the DOM without ANYONE being able to detect it.
Sub-Goal 2 - Give the script element a SECRET key (that NO other party can read) WITHOUT using the server side
Secondary Goal - Securely be able to identify the ACTUAL PARTY of the current script (ie not just the context it's from). As well, identify all parties in the call path of the currently running script in order to verify that no external party is making the current function call.
Actions I'm taking - Add a script element to the DOM (HEAD), with src = external script. For sub-goal 2, I'm appending a url parameter = key to the src url of the loaded script.
Detection Mechanisms Used to Defeat Security:
Poller
setInterval for 1 milli and get all "script" elements on page, search for the new element.
Listeners
ALL Dispatched Events in this process (and thus listened for, listeners attached to the HEAD)
- readyStateChange
- load
- DOMSubtreeModified
- DOMNodeRemoved
- DOMNodeInserted
- ?? DOMNodeInsertedIntoDocument
- ?? DOMNodeRemovedFromDocument
?? are these supposed to be dispatched?? in no instance did I detect them. I attached these particular listeners to the document as well.
Browser Support
Minimum Targeted Browsers:
FF 3.6+, IE7+, Safari 5.2+, Chrome 12+
Actual Tests Run on:
FF 3.6, FF 4, FF 6, FF 17, Safari 6.0, Chrome 18, IE7, IE8, IE9, IE10
Non-Targeted or Tested, But Likely to Work on due to what I read:
FF2.0+, IE5+, all Chrome
Critical Feature Support with Tested Browsers
DOM Mutation Events : all except IE8-
ReadyStateChange attribute on script element: IE5+
addEventListener : all except IE8-
document.currentScript : FF4+
Error.stack : all except IE9-
Removal of SCRIPT Element from DOM does NOT affect the running/event dispatching of the script : all except IE8-
Results : Secondary Goal
This goal is made possible through the call stack which exists on Error objects. With the Error.stack property, ALL scripts in the call stack are listed, including their SOURCE, function name, and line number. If this Error.stack string is parsed, then the actual URL of the parties in the stack can be accurately determined. The current party would be the top entry in this stack. The party which initiated the call would be the bottom most party. Thus satisfying Goal 3.
Restricted to browser with Error.stack.
Results : Sub-Goal 2
Add a secret key to the URL of the loaded script. Add the element to the DOM. Immediately change the src to something else. The loaded script can later pick up the original URL from the stack trace. This isn't really intended to be used in conjunction with the other methods on this page, as it would be a bit "overkill". You could though, if you were paranoid.
With the Error.stack property, all scripts in the call stack are listed, including their SOURCE, function name, and line number. Critical in this is that the source mentioned in the stack is unrelated to the current "src" attribute of the SCRIPT element to which the script belongs, meaning that if the "src" property is changed on the Element, then the call stack itself is the ONLY way to access the original source URL from which the script was loaded. If the loaded script is protected - meaning it has no globally accessible functions and no callbacks which can be accessed by a page script - then this URL becomes private between: the script which set the source, the external script itself, and any script which the external script calls. Thus satisfying Goal 2.
Also key to this is that no other party be able to access the Element's "src" before it can be changed (hidden) and that even after changed, the original "src" is the only URL actually used. This is arbitrary in non-IE, but in IE, you have to let the call stack complete before changing anything on the Element. My research confirmed that the "DOMNodeInserted" event on the new SCRIPT element is the first event thrown when adding the element to the DOM, and thus is the first place possible to change the "src" property. It appears that doing so here does not affect the URL which is loaded (the new URL is ignored by the browser) so no additional work is needed. For browsers without DOM Mutation Events, the Poller is the only way** to detect new Elements added to the DOM, therefore as long as the SCRIPT element is removed in the same call stack as it is added, then the Poller cannot detect the addition.
Goal success restricted to browser with Error.stack.
** as well as onpropertychange, but since this does not get dispatched till the element is removed and contains no reference to the new element, is not further considered.
Results : Sub-Goal 1
Sub-Goal 2 is lamost pointless if you can achieve Sub-Goal 1, since the existence of the new external script is completely hidden and it can be guaranteed that "load" event listeners on the element will run immediately after the external script. As such you could set a predetermined (preferably self-destructing) variable or function for the listener to access. As it is, Sub-Goal 1 cannot be completely achieved thanks to Firefox 5+ behaving differently than the other browsers, and the next paragraph is understood to exclude FF5+.
Basically, there are two ways to detect elements in the DOM - event listener and just scanning the DOM (Poller). Adding event listeners on the new SCRIPT element and stopping the event propagation is sufficient enough to block all DOMMutation events from reaching it's parent node on the DOM. Even DOMSubtreeModified does not get dispatched on the HEAD element since the added SCRIPT element is removed in the bubbling state of it's DOMNodeInserted event - likely there is some mechanism which checks for a difference in the DOM subtree, and when it finds none, does not dispatch a SubtreeModified event***. So with all events being stopped, the only mechanism remaining is the Poller, which as mentioned before cannot detect elements if they are added and removed in the same call stack - and fortunately adding and removing in this manner does not effect the load or function of the external script, except in IE8-, and in that case, repetitive adding-removing till the external script is eventually loaded works.
Firefox 5+, however, works differently. When stopping the propagation of the DOMNodeInserted/DOMNodeRemoved events, the DOMSubtreeModified is guaranteed to be dispatched on the parent (HEAD) element, likely due to a lack of comparison checking (as mentioned at ***). Since the event cannot be stopped, nor can it be guaranteed that it be canceled before it reaches another listener (FF12< don't have stopImmediatePropigation so it's actually guaranteed to reach all listeners), this is a failure. Listeners then have access to the SCRIPT element and can add listeners or change the "onload" property of the element (which would guarantee to run before other listeners). Therefore in this case, the additional security measure of Sub Goal 2 is needed to provide authenticity to the function caller.
Success restricted to browsers not FF5+
Primary Goal
The main goal would be easy if Sub - Goal 1 was complete. But it's not. It's a simple thing to create a private scope in both the loader and loaded script, the only complexity is how to provide for secure communication between two completely private scopes. This can be done thanks to event listeners. The loader, adding a listener for the loaded script, as long as it can maintain its position as only (or at least first) event listener, is guaranteed to run immediately after a loaded script runs. Therefore if the loaded script provides a one-time-only accessible function, then there can be secure interaction. All this hinges on the loader's callback being guaranteed to run first. For all browser which can provide for Sub Goal 1, this is no problem since another script is never given an opportunity to attach a listener. However, for FF, we have to find another way.
The problem isn't really with getting access to the SCRIPT Element, the problem is guaranteeing that the loader's listener gets called first. Now most browsers call back listeners in the order in which they register - but this is not part of the HTML spec, so cannot be relied on. The only thing we can rely on is that the "onload" property, if set to a function, will be called first. Therefore we have to guarantee that the loader's callback is that function (or at the very least that this property is empty so that no one can sneak in front of the loader's callback). I have seen, in FF at least, the peculiar behavior that only if a "onload" is set when the element is added to the DOM, will the "onload" be called after the loaded script runs. Did you get that? Therefore simply by not setting the "onload" property we deny that ability to would-be hijackers. If you'll notice, all security feature have so far been on the loader's side, but if one was inclined to, needlessly and just for piece of mind, one could set the onload property of the document.currentScript to null from the inside the loaded JS.
This does not prevent other scripts from adding an Event listener and hoping that it is called first, but, after rummaging around the Firefox source code, I confirmed that listeners are stored in a simple array and would always be run "in order". (Of course FF could change their source at any time, and in this, what I would consider so unlikely as to not need to be considered, event, we could use the security key of Sub Goal 2, for the loaded script to deny all access to those without that key. True, the hijacking script, if all these unlikely things happen, could then remove that exposing function, blocking all access to the loaded script, at least it can't do anything worse. Even if it removed the access function and replaced it with its own, thanks to Goal 2, to create some sort of callback which forces the call stack back into the loader script where it can verify the identity of the function which it is calling, is trivial to do - again, worse case scenario, only blocking the consumption of the loaded script.) So as things currently stand, even though the element get's exposed in FF5+, and FF10- can't stop event propagation immediately, still ok, because listeners get run in order and given no "onload" property, the first will always be the intended script.
So basically even though we can make it more secure using a private password and stack detection, we don't need to. However, stack detection will be key in security post-load.
Notable Notes
FF 3.5 - 3.6.x DONT load inserted script elements asynchronously. They make the whole page wait. Fix = add "defer" attribute to the element.
FF <3.5 still dont load an inserted script asynchronously, but there's no fix for that.
IE<=8 you can't "delete" references on the "window" object, 'cuz that throws an exception. And declared variables on that window object are not enumerable, which makes that first part "ok" as far as this project is concerned.
Tricks
add and remove it, over and over, never letting the element stay in the DOM outside the call stack - this eventually loads the script in IE 8-
1
2
3
4
5
6
7
8
9
10
11
12
13
|
document.head.appendChild(loadedScript.element);
if (loadedScript.element.parentNode){
document.head.removeChild(loadedScript.element);
( function M()
{
if (!loadedScript.isLoaded){
document.head.appendChild(loadedScript.element);
loadedScript.element = URL+ "" ;
document.head.removeChild(loadedScript.element);
setTimeout(M, 10);
}
})();
}
|
oddly, the onreadystatechange event is dispatched first before the loaded script has run!! OMg! So the listeners must be made to detect whether the loaded script has run or not.
get the URL of the current script (or any script in the call stack) when Error.stack exists
function (){
var S;
try {_} catch (e){
if (S=e.stack)
return doSomething(/:([^:]+\.[^:]+):/.exec(S.substr(S.lastIndexOf(S.indexOf( '@' )>=0? '@' : ' at ' )))[1]);
}
}
|
IE9- sucks because can't do this. Awesome, right?