I am writing a privacy extension which requires me to spoof the user agent property of the browser aka navigator.userAgent (yes I already know about the User-Agent HTTP header and have already dealt with that).
My issue is that a page might not just have a main frame but also a variable about of iframes as well. In my manifest file i am using all_frames: true to inject my content script into all frames and match_about_blank: true to inject into frames with a url of "about:blank".
I am using BrowserLeaks to test my extension it seems to spoof the user agent correctly using the window option but when using the iframe.contentWindow method it shows the real user agent.
I believe it might be because the iframe is sandboxed and you are not allowed to inject into sandboxed iframes. This would be a huge problem since sites could evade extensions and deny them access to a sandboxed iframe.
This is the error I get on Chromium:
Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.
From Chrome Developer
match_about_blank:
Optional. Whether to insert the content script on about:blank and about:srcdoc. Content scripts will only be injected on pages when their inherit URL is matched by one of the declared patterns in the matches field. The inherit URL is the URL of the document that created the frame or window.
Content scripts cannot be inserted in sandboxed frames.
Defaults to false.
Or perhaps the script is running in all iframes including the sandboxed ones but the script is not running quickly enough i.e. not run_at: document_start.
From MDN
match_about_blank:
match_about_blank is supported in Firefox from version 52. Note that in Firefox, content scripts won't be injected into empty iframes at "document_start" even if you specify that value in run_at.
My title says chrome extension however it is going to be for Firefox too. I posted documentation from both MDN and Chrome since their wording is different. On Chrome when I test this on say github.com I get errors regarding sandboxing on iframe however on Firefox I get no errors of such, however it still doesn't spoof the property inside the iframe like I want it to. Any ideas?
manifest.json
{
"name": "Shape Shifter",
"version": "0.0.1",
"description": "Anti browser fingerprinting web extension. Generates randomised values for HTTP request headers and javascript API's.",
"manifest_version": 2,
"icons": {
"16": "icons/crossed_eye_16x16.png",
"32": "icons/crossed_eye_32x32.png",
"48": "icons/crossed_eye_48x48.png",
"128": "icons/crossed_eye_128x128.png"
},
"background": {
"persistent": true,
"scripts": ["js/background.js"]
},
"browser_action": {
"default_title": "Shape Shifter",
"default_icon": {
"16": "icons/crossed_eye_16x16.png",
"32": "icons/crossed_eye_32x32.png"
},
"default_popup": "html/popup.html"
},
"content_scripts": [
{
"all_frames": true,
"match_about_blank": true,
"run_at": "document_end",
"matches": ["<all_urls>"],
"js": ["js/inject.js"]
}
],
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"web_accessible_resources": [
"js/lib/seedrandom.min.js",
"js/random.js",
"js/api/document.js",
"js/api/navigator.js",
"js/api/canvas.js",
"js/api/history.js",
"js/api/battery.js",
"js/api/audio.js",
"js/api/element.js"
]
}
inject.js (My content script)
console.log("Content Script Running ...");
function inject(filePath, seed) {
// Dynamically create a script
var script = document.createElement('script');
// Give the script a seed value to use for spoofing
script.setAttribute("data-seed", seed);
// Give the script a url to the javascript code to run
script.src = chrome.extension.getURL(filePath);
// Listen for the script loading event
script.onload = function() {
// Remove the script from the page so the page scripts don't see it
this.remove();
};
// Add the script tag to the DOM
(document.head || document.documentElement).appendChild(script);
}
function getSeed(origin) {
// Get a Storage object
var storage = window.sessionStorage;
// Try to get a seed from sessionStorage
var seed = storage.getItem(origin);
// Do we already have a seed in storage for this origin or not?
if (seed === null) {
// Initialise a 32 byte buffer
seed = new Uint8Array(32);
// Fill it with cryptographically random values
window.crypto.getRandomValues(seed);
// Save it to storage
storage.setItem(origin, seed);
}
return seed;
}
var seed = getSeed(window.location.hostname);
inject("js/lib/seedrandom.min.js", seed);
console.log("[INFO] Injected Seed Random ...");
inject("js/random.js", seed);
console.log("[INFO] Injected Random ...");
inject("js/api/document.js", seed);
console.log("[INFO] Injected Document API ...");
inject("js/api/navigator.js", seed);
console.log("[INFO] Injected Navigator API ...");
inject("js/api/canvas.js", seed);
console.log("[INFO] Injected Canvas API ...");
inject("js/api/history.js", seed);
console.log("[INFO] Injected History API ...");
inject("js/api/battery.js", seed);
console.log("[INFO] Injected Battery API ...");
inject("js/api/audio.js", seed);
console.log("[INFO] Injected Audio API ...");
inject("js/api/element.js", seed);
console.log("[INFO] Injected Element API ...");
I was able to get around this issues in the following way:
From my content script ...
var UAScript = "document.addEventListener('DOMContentLoaded', function(event) { var iFrames = document.getElementsByTagName('iframe'); "+
"for (i=0; i<iFrames.length; i++) try { Object.defineProperty(iFrames[i].contentWindow.clientInformation, 'userAgent', { value:'Custom' }); } catch(e) {} }); "+
"try { Object.defineProperty(clientInformation, 'userAgent', { value:'Custom' }); } catch(e) {}";
var script = document.createElement("script");
script.type = "text/javascript";
script.textContent = UAScript;
document.documentElement.appendChild(script);
This changes both the userAgent on the document, plus the iFrames after the DOMContentLoaded triggers.
Question is ..., is there a better way of doing this?
(Because you can still get around this spoof by adding an iframe inside an iframe, etc.)
Related
I want to know whether the current tab is an old tab which was opened before installing the extension or is that a special tab (browser UI, extension page, chrome.google.com) where I can not inject the content script.
There's a partial solution where I try to send message to the content script and if it throws an error (i.e. content script is not loaded on the page) then it's either an old tab or a special page. I need a way to know which one is it so that I can inform via popup page.
Detecting if browser extension popup is running on a tab that has content script
There is possibly one more way: try to execute script on page, if it succeeds then it was an old tab but this would need one more permission in manifest i.e. scripting which I feel is a bit excessive just to detect an old tab. any other possible solutions?
This is for chrome extension development.
If you're only interested in distinguishing new tabs from tabs that are old and/or non-injectable, you can let the content scripts add the IDs of their tabs to session storage. Later on, you can look up the ID of any tab in session storage.
old tab = tab that was opened before installing the extension
new tab = tab that was opened after installing the extension
non-injectable tab = tab that you can't inject content scripts into, see Chrome Extension postMessage from background script to content script every time in change crome tabs
Tab IDs are only valid during the current session. When you store tab IDs in sessions storage, they are gone when you start a new session, which is what you want.
Content scripts don't know the ID of the tab they're running in, so they can't store it by calling chrome.storage.session.set() directly. However, content scripts can send a message to the service worker. The service worker receives information about the sender's tab along with the message.
The proof of concept below doesn't try to determine if a tab is injectable or not.
You can either do this by checking the tab's URL, e.g. if it starts with "chrome://" or "chrome-extension://". But I don't know if you can determine all non-injectable tabs like this, e.g. those whose URL is forbidden by the runtime_blocked_hosts policy.
Or you can inject an empty content script into the tab and check for errors. This requires the "scripting" permission, plus "activeTab" or the right host permissions, but lets you determine all non-injectable tabs.
When you click the action, the extension shows a notification. It tells you if the active tab is old, or if it's new and/or non-injectable.
manifest.json
{
"manifest_version": 3,
"name": "Tabs with vs without Content Scripts",
"version": "1.0",
"action": {
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content_script.js"]
}
],
"permissions": [
"notifications",
"storage"
]
}
background.js
async function action_on_clicked(tab) {
let { [tab.id.toString()]: tab_id_stored } = await chrome.storage.session.get(tab.id.toString());
let message;
if (tab_id_stored === undefined) {
/* Old or non-injectable tab */
message = "Old or non-injectable tab";
}
else {
message = "New tab";
}
chrome.notifications.create(
{
iconUrl: "/icon_128.png",
message,
title: "Tabs with vs without Content Scripts",
type: "basic",
},
notificationId => {
if (chrome.runtime.lastError === undefined) {
console.log(notificationId);
}
else {
console.error(chrome.runtime.lastError);
}
}
);
}
function runtime_on_message(message, sender, sendResponse) {
if (message == "store_tab_id") {
if (sender.tab) {
chrome.storage.session.set({ [sender.tab.id.toString()]: true })
.then(() => {
sendResponse("tab id stored");
})
.catch(error => {
sendResponse(error);
});
return true;
}
else {
sendResponse("sender.tab is undefined");
}
}
else {
sendResponse("unknown message");
}
}
chrome.action.onClicked.addListener(action_on_clicked);
chrome.runtime.onMessage.addListener(runtime_on_message);
content_script.js
(async () => {
let response = await chrome.runtime.sendMessage("store_tab_id");
console.log("response", response);
})();
I've had this extension in the Google Chrome store for a while now. After doing a maintenance update I noticed that the following line from the content.js (content script):
//Get top document URL (that is the same for all IFRAMEs)
var strTopURL = window.top.document.URL;
is now throwing the following exception when the loaded page has an IFRAME in it:
Blocked a frame with origin "https://www.youtube.com" from accessing a
cross-origin frame.
Like I said, it used to be the way to obtain the top document URL for your extension (from the content script). So what's the accepted way to do it now?
PS. Again, I'm talking about a Google Chrome extension (and not just a regular JS on the page.)
EDIT: This script is running under the content_scripts in the manifest.json that is defined as such:
"content_scripts": [
{
"run_at": "document_end",
"all_frames" : true,
"match_about_blank": true,
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
],
The content script should ask your background script to do it via messaging:
chrome.runtime.sendMessage('getTopUrl', url => {
// use the URL here inside the callback or store in a global variable
// to use in another event callback that will be triggered in the future
console.log(url);
});
// can't use it right here - because the callback runs asynchronously
The background script should be declared in manifest.json:
"background": {
"scripts": ["background.js"],
"persistent": false
},
You'll also need need specific URL permissions in manifest.json or allow all URLs:
"permissions": ["<all_urls>"]
And the listener in the background script:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getTopUrl') {
chrome.tabs.get(sender.tab.id, tab => sendResponse(tab.url));
// keep the message channel open for the asynchronous callback above
return true;
}
});
My extension's content script injects a script into gmail page via <script> element (main.js). The injected script needs some data from the settings stored in the extension's localStorage by options.js script of the options page.
The options page script can successfully use loadDomain() function that reads localStorage.domain value. This function is defined in a common functions script storage.js that is also injected on gmail page via <script> element along with main.js.
The problem is that loadDomain() returns undefined when called in the injected main.js instead of the actual values stored on the options page.
manifest.json:
"permissions": [
"tabs", "https://mail.google.com/*", "http://*/*, https://*/*"
],
"background": {
"scripts": ["js/background.js"],
"persistent": false
},
"browser_action": {
"default_icon": {
"38": "icon.png"
},
"default_title": "SalesUp",
"default_popup": "index.html"
},
"content_scripts": [
{
"matches": ["https://mail.google.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
"js/jquery-1.10.2.min.js",
"js/gmail.js",
"main.js"
]
}
The chat discussion showed that loadDomain() was invoked from the <script>-injected main.js.
One part of the problem was caused by the fact that Chrome isolates the web page (with its scripts, also the injected ones) from the content scripts, as well as the background page. Another part was that localStorage is different on each domain (actually, origin), so whatever was stored inside the options page of the extension was not available in a content script that runs in the context of the web page and has access to its localStorage only, not the extension's localStorage.
The solution comprises two things:
instead of localStorage use chrome.storage.sync to store the extension settings or chrome.storage.local to store the temporary stuff that shouldn't be synced to Google servers.
use custom DOM-events to communicate between the injected script and the content script.
The code:
Injected main.js:
sending a request (detail key may be used to pass some data):
document.dispatchEvent(new CustomEvent("getDomains", {detail: {something: "hello"}}));
listening for a response:
document.addEventListener("receiveDomains", function(e) {
var domains = e.detail;
console.log("Received domains", domains);
...............
});
Content script content.js:
document.addEventListener("getDomains", function(e) {
chrome.storage.sync.get("domains", function(result) {
document.dispatchEvent(new CustomEvent("receiveDomains", {detail: result.domains}));
});
});
Options page options.js:
function save_options() {
chrome.storage.sync.set({domains: ["domain1", "domain2"]});
}
Is there a way to access the list of resources that the browser requested (the ones found in this Chrome inspector's network panel)?
I would like to be able to iterate through these fetched resources to show the domains that have been accessed, something like:
for (var i = 0; i < window.navigator.resources.length; i++) {
var resource = window.navigator.resources[i];
console.log(resource); //=> e.g. `{domain: "www.google-analytics.com", name: "ga.js"}`
}
Or, maybe there is some event to write a handler for, such as:
window.navigator.onrequest = function(resource) {
console.log(resource); //=> e.g. `{domain: "www.google-analytics.com", name: "ga.js"}`
}
It doesn't need to work cross browser, or even be possible using client-side JavaScript. Just being able to access this information in any way would work (maybe there's some way to do this using phantomjs or watching network traffic from a shell/node script). Any ideas?
You can do this, but you will need to use Chrome extensions.
Chrome extensions have a lot of sandbox-style security. Communication between the Chrome extension and the web page is a multi-step process. Here's the most concise explanation I can offer with a full working example at the end:
A Chrome extension has full access to the chrome.* APIs, but a Chrome extension cannot communicate directly with the web page JS nor can the web page JS communicate directly with the Chrome extension.
To bridge the gap between the Chrome extension and the web page, you need to use a content script . A content script is essentially JavaScript that is injected at the window scope of the targeted web page. The content script cannot invoke functions nor access variables that are created by the web page JS, but they do share access to the same DOM and therefore events as well.
Because directly accessing variables and invoking functions is not allowed, the only way the web page and the content script can communicate is through firing custom events.
For example, if I wanted to pass a message from the Chrome extension to the page I could do this:
content_script.js
document.getElementById("theButton").addEventListener("click", function() {
window.postMessage({ type: "TO_PAGE", text: "Hello from the extension!" }, "*");
}, false);
web_page.js
window.addEventListener("message", function(event) {
// We only accept messages from ourselves
if (event.source != window)
return;
if (event.data.type && (event.data.type == "TO_PAGE")) {
alert("Received from the content script: " + event.data.text);
}
}, false);
`4. Now that you can send a message from the content script to the web page, you now need the Chrome extension gather up all the network info you want. You can accomplish this through a couple different modules, but the most simple option is the webRequest module (see background.js below).
`5. Use message passing to relay the info on the web requests to the content script and then on to the web page JavaScript.
Visually, you can think of it like this:
Full working example:
The first three files comprise your Google Chrome Extension and the last file is the HTML file you should upload to http:// web space somewhere.
icon.png
Use any 16x16 PNG file.
manifest.json
{
"name": "webRequest Logging",
"description": "Displays the network log on the web page",
"version": "0.1",
"permissions": [
"tabs",
"debugger",
"webRequest",
"http://*/*"
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icon.png",
"default_title": "webRequest Logging"
},
"content_scripts": [
{
"matches": ["http://*/*"],
"js": ["content_script.js"]
}
],
"manifest_version": 2
}
background.js
var aNetworkLog = [];
chrome.webRequest.onCompleted.addListener(function(oCompleted) {
var sCompleted = JSON.stringify(oCompleted);
aNetworkLog.push(sCompleted);
}
,{urls: ["http://*/*"]}
);
chrome.extension.onConnect.addListener(function (port) {
port.onMessage.addListener(function (message) {
if (message.action == "getNetworkLog") {
port.postMessage(aNetworkLog);
}
});
});
content_script.js
var port = chrome.extension.connect({name:'test'});
document.getElementById("theButton").addEventListener("click", function() {
port.postMessage({action:"getNetworkLog"});
}, false);
port.onMessage.addListener(function(msg) {
document.getElementById('outputDiv').innerHTML = JSON.stringify(msg);
});
And use the following for the web page (named whatever you want):
<!doctype html>
<html>
<head>
<title>webRequest Log</title>
</head>
<body>
<input type="button" value="Retrieve webRequest Log" id="theButton">
<div id="outputDiv"></div>
</head>
</html>
Big shoutout to #Elliot B.
I essentially used what he did but I wanted events to trigger in the content script rather than listeners triggering in the background. For whatever reason, I was unable to connect to the port from the background script so this is what I came up with.
PS: you need jquery.js in the extension folder to make this work.
manifest.json
{
"manifest_version": 2,
"name": "MNC",
"version": "0.0.1",
"description": "Monitor Network Comms",
"permissions":["webRequest","*://*/"],
"content_scripts": [{
"matches": ["<all_urls>"],
"run_at": "document_start",
"js": ["content.js",
"jquery.js"]
}],
"background": {
"scripts": ["background.js"]
}
}
background.js
var aNetworkLog = [];
chrome.webRequest.onResponseStarted.addListener(
function(oCompleted) {
var sCompleted = JSON.stringify(oCompleted);
aNetworkLog.push(sCompleted);
},{urls: ["https://*/*"]}
);
chrome.extension.onConnect.addListener(function (port) {
chrome.webRequest.onResponseStarted.addListener(
function(){
port.postMessage({networkLog:JSON.stringify(aNetworkLog)});
},{urls: ["https://*/*"]}
);
port.onMessage.addListener(function (message) {
if (message.disconnect==true) {
port.disconnect();
}
});
});
content.js
div = $('<div id="outputDiv" style="float:left;max-width:fit-content;position:fixed;display:none;"></div>').appendTo(document.body);
var port = chrome.extension.connect({name:'networkLogging'});
port.onMessage.addListener(function (message) {
if (message.networkLog) {
div[0].innerHTML = message.networkLog;
}
});
observer = new WebKitMutationObserver(function(mutation,observer){
JSON.parse(mutation[0]['target'].innerHTML).forEach(function(item){
JSON.parse(item);
})
});
observer.observe(div[0],{childList:true});
This is definitely not the most efficient way of doing things but it works for me. Thought that I would add it in here just in case someone is needing it.
I'm doing a plugin to do some transformations to the interface. I keep getting unsafe javascript attempt to access frame with url.... Domains, protocols and ports must match (typical cross site issue)
But being an extension it should have access to the iframe's content http://code.google.com/chrome/extensions/content_scripts.html ...
Doesn anyone know how to access it's contents so they can be capturable?
There's generally no direct way of accessing a different-origin window object. If you want to securely communicate between content scripts in different frames, you have to send a message to the background page which in turn sends the message back to the tab.
Here is an example:
Part of manifest.json:
"background": {"scripts":["bg.js"]},
"content_scripts": [
{"js": ["main.js"], "matches": ["<all_urls>"]},
{"js": ["sub.js"], "matches": ["<all_urls>"], "all_frames":true}
]
main.js:
var isTop = true;
chrome.runtime.onMessage.addListener(function(details) {
alert('Message from frame: ' + details.data);
});
sub.js:
if (!window.isTop) { // true or undefined
// do something...
var data = 'test';
// Send message to top frame, for example:
chrome.runtime.sendMessage({sendBack:true, data:data});
}
Background script 'bg.js':
chrome.runtime.onMessage.addListener(function(message, sender) {
if (message.sendBack) {
chrome.tabs.sendMessage(sender.tab.id, message.data);
}
});
An alternative method is to use chrome.tabs.executeScript in bg.js to trigger a function in the main content script.
Relevant documentation
Message passing c.runtime.sendMessage / c.tabs.sendMessage / c.runtime.onMessage
MessageSender and Tab types.
Content scripts
chrome.tabs.executeScript
I understand that this is an old question but I recently spent half a day in order to solve it.
Usually creating of a iframe looks something like that:
var iframe = document.createElement('iframe');
iframe.src = chrome.extension.getURL('iframe-content-page.html');
This frame will have different origin with a page and you will not be able to obtain its DOM. But if you create iframe just for css isolation you can do this in another way:
var iframe = document.createElement('iframe');
document.getElementById("iframe-parent").appendChild(iframe);
iframe.contentDocument.write(getFrameHtml('html/iframe-content-page.html'));
.......
function getFrameHtml(htmlFileName) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", chrome.extension.getURL(html/htmlFileName), false);
xmlhttp.send();
return xmlhttp.responseText;
}
.......
"web_accessible_resources": [
"html/htmlFileName.html",
"styles/*",
"fonts/*"
]
After that you can use iframe.contentDocument to access to iframe's DOM