Path: blob/master/modules/misc/iframe_sniffer/leakyframe.js
1154 views
/**1* LeakyFrame JS Library2*3* Copyright (c) 2012 Paul Stone4*5* Permission is hereby granted, free of charge, to any person obtaining a copy of this6* software and associated documentation files (the "Software"), to deal in the Software7* without restriction, including without limitation the rights to use, copy, modify,8* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to9* permit persons to whom the Software is furnished to do so, subject to the following10* conditions:11*12* The above copyright notice and this permission notice shall be included in all copies13* or substantial portions of the Software.14*15* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,16* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A17* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT18* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION19* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE20* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.21*22*/232425/*2627This JS library can be used to easily try out the Framesniffing (aka Frame Leak, aka Anchor Element Position Detection) technique.28Currently (as of Mar 2012) the technique works in IE8, IE9 and most29webkit-based browsers.3031Example usage:3233new LeakyFrame('http://example.com', function(frame) {34if (frame.checkID('login-form')) {35alert("You're not logged in");36frame.remove();37return;38}3940var anchors = {'foo', 'bar', 'baz'};41var frags = frame.findFrags(anchors);42alert('Found the following anchors on the page: ' + frags.join(', '));43}444546Redirects47---------48Make sure that the URL you're loading doesn't redirect to a different URL,49as this can break anchor checking in some browsers and will slow down50checks for multiple anchors (e.g. brute-forcing).5152E.g. You create LeakyFrame with http://foo.com/somepage and it redirects53to http://foo.com/somepage?987239455455The reason for this is that the JS code can't know the URL that the frame56has redirected to (due to same-origin policy). When changing the #fragment57at the end of the URL to check for an anchor, the entire URL must be58reset using window.location. So if a redirect has occurred, the original59URL will be loaded in the iframe, causing a full page load and another60redirect to occur.6162Some browsers will preserve URL fragments across page redirects (Chrome63does, and I think IE10 does now too). For those browsers you can create64a LeakyFrame and pass in a URL with an fragment already on the end, then65call frame.nonZero() to see if a scroll has occurred. The findManyMatchingURLs66and findFirstMatchingURL methods should also work with redirects.6768*/6970/**71* Creates a new LeakyFrame object72*73* This constructor creates a nested iframes and loads 'url' into the inner one74* The outer frame is 10x10, and the inner frame 1000x10,000px to force the outer75* frame to scroll when checking for anchors.76*77* @param url - URL to load into the iframe.78* @param callback - A function that will be called when the frame has loaded. The79* the callback function will be passed the newly created LeakyFrame object80* @param debug - If true, the created frames will be made visible and outer81* frame will be made larger (140x140)82*/83function LeakyFrame(url, callback, debug) {84var outer = document.createElement('iframe');85outer.setAttribute('frameBorder', '0');86outer.setAttribute('scrolling', 'no')87document.body.appendChild(outer);8889outer.contentWindow.document.open();90outer.contentWindow.document.close();9192var inner = outer.contentWindow.document.createElement('iframe');93inner.setAttribute('frameBorder', '0');9495outer.contentWindow.document.body.style.margin = 0;96outer.contentWindow.document.body.style.padding = 0;97inner.setAttribute('style', 'margin:0; border:none;overflow:hidden;position:absolute; left:0px;top:0px;width:1000px;height:10000px;background-color:white;');9899if (!debug)100outer.setAttribute('style', 'border:none;opacity:0');101102outer.contentWindow.document.body.appendChild(inner);103104outer.width = 10;105outer.height = 10;106if (debug) {107outer.width=140;108outer.height=140;109}110this.outer = outer; // outer iframe element111this.inner = inner; // inner iframe element112this.innerWin = inner.contentWindow; // window object of outer iframe113this.outerWin = outer.contentWindow; // window object of inner iframe114this.outerDoc = outer.contentWindow.document; // document of outer iframe115this.removed = false;116if (callback)117this.load(url, callback);118}119120/**121* Load a new URL into the inner iframe and do a callback when it's loaded122*/123LeakyFrame.prototype.load = function(url, callback) {124this.inner.contentWindow.location = url;125var me = this;126var f = {};127f.fn = function() {128if (me.inner.removeEventListener)129me.inner.removeEventListener('load', f.fn);130else if (me.inner.detachEvent)131me.inner.detachEvent('onload', f.fn);132133me.currentURL = me._stripFragment(url);134if (callback)135callback(me);136}137if (this.inner.addEventListener)138this.inner.addEventListener('load', f.fn, false);139else if (this.inner.attachEvent)140this.inner.attachEvent('onload', f.fn);141}142143144/**145* Find the current scroll position of the outer iframe146* (should correspond to the position of the current anchor147* in the inner iframe)148* @return object with .x and .y properties149*/150LeakyFrame.prototype.getPos = function() {151var x = this.outerDoc.body.scrollLeft;152var y = this.outerDoc.body.scrollTop;153return {x:x, y:y};154}155156157/**158* Reset the scroll position of the iframe159*/160LeakyFrame.prototype.resetPos = function() {161this.outerWin.scrollTo(0,0);162}163164/**165* Checks if the iframe has scrolled after being reset166*/167LeakyFrame.prototype.nonZero = function() {168var pos = this.getPos();169return (pos.x > 0 || pos.y > 0);170};171172/**173* Check if anchor 'id' exists on the currently loaded page174* This works by first resetting the scroll position, adding175* #id onto the end of the current URL and then seeing if176* the scroll position has changed.177*178* Optional parameters x and y specify the initial scroll179* position and default to 0. Useful in some cases where180* weird scrolling behaviour causes the page to scroll to181* (0,0) if an anchor is found.182*183* @return boolean - true if the anchor exists184*/185LeakyFrame.prototype.checkID = function(id, x, y) {186if (!x) x = 0;187if (!y) y = 0;188this.outerWin.scrollTo(x,y);189this.innerWin.location = this.currentURL + '#' + id;190var result = this.getPos();191return (result.x != x || result.y != y);192}193194/**195* Given an array of ids, will check the current page to see which196* corresponding anchors exist. It will return a dictionary of197* positions for each matched anchor.198*199* This can be incredibly quick in some browsers (checking 10s or200* 100s of IDs per second), so could be used for things like201* brute-forcing things like numeric user IDs or lists of product202* codes.203*204* @param ids - an array of IDs to be checked205* @param templ - optional template which is used to make the URL206* fragment. If the template is 'prod{id}foo' and you pass in the207* array [5,6,7], then it will check for anchors:208* prod5foo, prod6foo and prod7foo209* @return a dictionary containing matched IDs as keys and an210* array [x,y] as values211*/212LeakyFrame.prototype.findFragPositions = function(ids, templ) {213this.outerWin.scrollTo(0,0);214if (templ) {215var newids = [];216for (var i = 0; i < ids.length; i++)217newids.push(templ.replace('{id}', ids[i]));218} else {219newids = ids;220}221var positions = {};222for (var i = 0; i < ids.length; i++) {223var id = newids[i];224//this.outerWin.scrollTo(0,0);225this.innerWin.location = this.currentURL + '#' + id;226var x = this.outerDoc.body.scrollLeft;227var y = this.outerDoc.body.scrollTop;228229if (x || y) {230positions[ids[i]] = [x, y];231this.outerWin.scrollTo(0,0);232}233}234return positions;235}236237/**238* Same as findFragPositions but discards the positions239* and returns only an array of matched IDs.240*/241LeakyFrame.prototype.findFrags = function(ids, templ) {242var found = this.findFragPositions(ids, templ);243var ids = [];244for (var id in found)245ids.push(id);246return ids;247}248249LeakyFrame.prototype._stripFragment = function(url) {250var pos = url.indexOf('#');251if (pos < 0) return url;252this.loadFrag = url.substr(pos+1);253return url.substr(0, pos)254}255256/**257* Removes the iframe from the document.258* If you're creating lots of LeakyFrame instances259* you should call this once you're done with each260* frame to free up memory.261*/262LeakyFrame.prototype.remove = function() {263if (this.removed) return;264this.outer.parentNode.removeChild(this.outer);265this.removed = true;266}267268269/**270* Load a bunch of URLs to find which ones have a matching fragment271* (i.e. cause the page to scroll). (static method)272*273* @param url - a base URL to check with {id} where id will go274* (e.g http://www.foo.com/userprofile/{id}#me)275* @param ids - dictionary of key:value pairs. the keys will be276* used to replace {id} in each url277* @param callback - a function that gets called when an id is found.278* It gets passed the matching key and value279* @param finishedcb - a function that gets called when all the urls280* have been checked. It gets passed true if any URLs were matched281*/282LeakyFrame.findManyMatchingURLs = function(url, ids, callback, finishedcb, stopOnFirst) {283var maxConcurrent = 3;284var inProgress = 0;285var todo = [];286var interval;287var loadCount = 0;288var allFound = {};289var allPositions = {};290var framePool = {};291var found = 0;292var cancelled = false;293for (var key in ids) {294todo.push(key);295loadCount++;296}297298var cancel = function() {299cancelled = true;300for (var i in framePool)301framePool[i].remove();302if (interval)303window.clearInterval(interval);304}305306var cb = function(f, foundFrag) {307inProgress--;308loadCount--;309310if (f.nonZero()) {311found++;312var foundVal = ids[foundFrag];313var foundPos = f.getPos();314allFound[foundFrag] = foundVal;315allPositions[foundFrag] = foundPos;316317if (!cancelled)318callback(foundFrag, foundVal, foundPos, allFound, allPositions);319if (stopOnFirst)320cancel();321}322if ((loadCount == 0 && !stopOnFirst) || // 'finished' call for findMany323(loadCount == 0 && stopOnFirst && found == 0)) // 'fail' callback for stopOnFirst (only if none were found)324finishedcb(found > 0, allFound, allPositions);325f.remove();326delete framePool[foundFrag];327}328329var loadMore = function() {330if (todo.length == 0) {331// no more ids to do332window.clearInterval(interval);333interval = null;334}335if (inProgress >= maxConcurrent) {336// queue full, waiting337return;338}339var loops = Math.min(maxConcurrent - inProgress, todo.length);340for (var i=0; i < loops; i++) {341inProgress++;342var nextID = todo.shift();343var thisurl = url.replace('{id}', nextID);344345framePool[nextID] = new LeakyFrame(thisurl, function(n){346return function(f){ setTimeout(function() {cb(f,n)}, 50) } // timeout delay required for reliable results on chrome347}(nextID)348);349}350}351interval = window.setInterval(loadMore, 500);352}353354355/**356* Same as findManyMatchingURLs but stops after the first match is found357*358* @param url - a base URL to check with {id} where id will go359* (e.g http://www.foo.com/userprofile/{id}#me)360* @param ids - dictionary of key:value pairs. the keys will be used to361* replace {id} in each url362* @param successcb - a function that gets called when an id is found.363* It gets passed the matching key and value364* @param failcb - a function that gets called if no ids are found365* @param finalcb - a function that gets called after either sucess or failure366*/367LeakyFrame.findFirstMatchingURL = function(url, ids, successcb, failcb, finalcb) {368var s = function(k, v) {369successcb(k, v);370if (finalcb)371finalcb();372}373var f = function() {374if (failcb)375failcb();376if (finalcb)377finalcb();378}379return LeakyFrame.findManyMatchingURLs(url, ids, s, f, true);380}381382383