It’s been known for many years that storing sensitive data in JavaScript files is not only a bad practice, but also a rather dangerous one. The reason for that is relatively simple. Let’s assume that you dynamically generate a JavaScript file containing the API key of your user.
apiCall = function(type, api_key, data) { ... } var api_key = '1391f6bd2f6fe8dcafb847e0615e5b29' var profileInfo = apiCall('getProfile', api_key, 'all')
Whenever you create a variable in the global scope like in the example above, you make it available to any website that includes your script file as well.
The reasons why developers would embed sensitive information in JavaScript files are wide ranging. For inexperienced developers this may be the only obvious way to pass information that was stored or generated on the server side to their client side code. It may also save some additional requests to the server. However, an often overlooked aspect of this is browser extensions. Sometimes it’s necessary to directly inject script tags into the DOM for it to use exactly the same window object. This wouldn’t be possible with content scripts alone.
We have talked about the global scope above. For JavaScript in browsers, a global variable is effectively a property of the window object. However, back in ECMA Script 5 there was only one additional scope, the function scope. That means if we declare a variable within a function using the var keyword, it is not globally available. With ECMA Script 6 an additional scope was introduced, the block scope and together with it the keywords const and let.
Both keywords are being used to declare a variable in a block scope, but you can not reassign variables that were created using const. If we omit the declaration with any of these keywords or if we use var outside of a function, we create a global variable and it is actually quite rare that we’d want to do that.
An effective way to prevent yourself from accidentally creating global variables is to activate strict mode. This can be done by adding the string “use strict” at the beginning of either a file or a function. It will then prevent you from using variables that weren’t declared before.
"use strict"; var test1 = 'arka' // works test2 = 'kapı' // Reference Error
You can use this in conjunction with so called Immediately Invoked Function Expressions (short IIFE, pronounced iffy). IIFEs can be used to create a function scope, but they immediately execute the function body. Let’s see how this looks like.
(function() { "use strict"; //variable declared within function scope var privateVar = 'Secret value'; })() console.log(privateVar) // Reference Error
On first glance this looks like an effective way to create variables whose content can’t be read outside of their scope. But don’t be fooled. While IIFEs are a good way to avoid polluting the global namespace, they aren’t completely suitable to protect their content.
It is (almost) impossible to keep the content of a private variable private. There are different reasons, some of which we will examine now. This is by far not an exhaustive list, but rather a reminder to show why you should never save sensitive data in your JavaScript files.
The most obvious reason to decide against this dangerous practice is that you actually want to use the value of a variable to carry out a certain task. In our first example we need this key to make a request to a server. And therefore we need to send it over the network in clear text. Now there aren’t an awful lot of ways to do this in JavaScript. Let’s say our code uses the fetch() function.
window.fetch = (url, options) => { console.log(`URL: ${url}, data: ${options.body}`); }; // EXTERNAL SCRIPT START (function(){ "use strict"; var api_key = "1391f6bd2f6fe8dcafb847e0615e5b29" fetch('/api/v1/getusers', { method: "POST", body: "api_key=" + api_key }); })() // EXTERNAL SCRIPT END
As you see, we can simply override the fetch function and then steal the API key that way. The only prerequisite is that we can include the external script after our own script block. In this example we just log it out, but of course we could send it to our own server as well.
Private variables may not only contain strings, but also objects or arrays. Objects can have different properties and in most of the cases you can simply set them and read their values. But JavaScript supports a pretty interesting functionality. You can actually execute a function if a property is set on an object or when it’s accessed. This works with the __defineSetter__ and __defineGetter__ functions. If we apply the __defineSetter__ function to the prototype of the Object constructor, we can effectively log every value that’s assigned to a property with a certain name.
Object.prototype.__defineSetter__('api_key', function(value){ console.log(value); return this._api_key = value; }); Object.prototype.__defineGetter__('api_key', function(){ return this._api_key; }); // EXTERNAL SCRIPT START (function(){ "use strict" let options = {} options.api_key = "1391f6bd2f6fe8dcafb847e0615e5b29" options.name = "Alice" options.endpoint = "get_user_data" anotherAPICall(options); })() // EXTERNAL SCRIPT END
If the code assigns a property to an object containing the API key, we can easily access it with our setter. The getter on the other hand will make sure that the rest of the code is working correctly. This is not absolutely necessary, but can sometimes be helpful.
After we took a look at strings that are passed to native functions and objects with setters / getters, it’s time to take a look at simple arrays. If the code iterates over an array with a for … of loop, we can define a custom iterator on the prototype of the Array constructor. This will allow us to access the content of the array and still maintain functioning code.
Array.prototype[Symbol.iterator] = function() { let arr = this; let index = 0; console.log(arr) return { next: function() { return { value: arr[index++], done: index > arr.length } } } }; // EXTERNAL SCRIPT START (function() { let secretArray = ["this", "contains", "an", "API", "key"]; for (let element of secretArray) { doSomething(element); } })() // EXTERNAL SCRIPT END
I’m not going to talk about the concept of iterators, since that’s a little bit out of scope here. What’s actually important is that we can access the whole array from within the custom Symbol.iterator method and therefore steal the secret value.
As mentioned before, this is not an exhaustive list of possibilities that would allow an attacker to steal secret values from your script files. Even IIFEs, strict mode and declaring variables in a function / block scope will not always help you. My recommendation is that you dynamically fetch sensitive data from the server instead of writing it into your JavaScript files. In most, if not all cases, this is a sane alternative and may even be more maintainable.