Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
349 views
in Technique[技术] by (71.8m points)

google chrome extension - Injecting javascript variable before content script

Using my background script background.js, I need to inject a dynamic variable as a content script before injecting another file inject.js also as a content script. Inject.js will need to have access to this variable and run it's code BEFORE any scripts on the page run. I am having difficulties accessing the dynamic variable from the inject.js content script.

manifest.json

{
"name": "Shape Shifter",
"version": "1.0",
"description": "Anti browser fingerprinting web extension. Generates randomised values for HTTP request headers, javascript property values and javascript method return types.",
"manifest_version": 2,
"icons": {
    "32": "icons/person-32.png",
    "48": "icons/person-48.png"
},
"background": {
    "persistent": true,
    "scripts": ["js/ua.js", "js/words.js", "js/lib/seedrandom.min.js", "js/random.js", "js/background.js"]
},
"browser_action": {
    "default_title": "Shape Shifter",
    "default_icon": "icons/person-32.png",
    "default_popup": "html/popup.html"
},
"content_scripts": [
  {
    "run_at": "document_end",
    "matches": ["<all_urls>"],
    "js": ["js/inject.js"] 
  }
],
"permissions": [
    "webRequest",
    "webRequestBlocking",
    "webNavigation",
    "tabs",
    "activeTab",
    "storage",
    "<all_urls>"
],
"web_accessible_resources": [
    "js/ua.js",
    "js/words.js",
    "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"
]

}

background.js

"use strict";

console.log("Background Script Running ...");

function getSeed(origin) {
    // Get a Storage object
    var storage = window.localStorage;

    // Do we already have a seed in storage for this origin or not?
    var seed = storage.getItem(origin);

    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;
}

// Methods to get HTTP headers
function getAcceptHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptCharsetHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptEncodingHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptLanguageHeader() {
    // NOTE: TOR Browser uses American English
    return "en-US,en;q=0.5";
}
function getAuthorizationHeader(seed) {
    return "NotYetImplemented";
}
function getExpectHeader(seed) {
    return "NotYetImplemented";
}
function getFromHeader(seed) {
    return "NotYetImplemented";
}
function getHostHeader(seed) {
    return "NotYetImplemented";
}
function getIfMatchHeader(seed) {
    return "NotYetImplemented";
}
function getIfModifiedSinceHeader(seed) {
    return "NotYetImplemented";
}
function getIfNoneMatchHeader(seed) {
    return "NotYetImplemented";
}
function getIfRangeHeader(seed) {
    return "NotYetImplemented";
}
function getIfUnmodifiedSinceHeader(seed) {
    return "NotYetImplemented";
}
function getMaxForwardsHeader(seed) {
    return "NotYetImplemented";
}
function getProxyAuthorizationHeader(seed) {
    return "NotYetImplemented";
}
function getRangeHeader(seed) {
    return "NotYetImplemented";
}
function getRefererHeader() {
    // NOTE: From https://developer.mozilla.org/en-US/docs/Web/API/Document/referrer
    // NOTE: The value is an empty string if the user navigated to the page directly (not through a link, but, for example, via a bookmark).
    // NOTE: Since this property returns only a string, it does not give you DOM access to the referring page.

    // NOTE: Make websites think we always go to them directly rather than being referred.
    return "";
}
function getTEHeader(seed) {
    return "NotYetImplemented";
}
function getUserAgentHeader(seed) {
    Math.seedrandom(seed);

    return userAgents[randomNumber(0, userAgents.length)];
}

function rewriteHttpHeaders(e) {
    // Create URL object from url string
    var serverUrl = new URL(e.url);

    console.log(e);

    // Get the origin (hostname)
    var origin = serverUrl.hostname;

    var seed = getSeed(origin);

    console.log("Background - Seed for origin " + origin + ": " + seed);

    for (var header of e.requestHeaders) {
        if (header.name.toLowerCase() === "accept") {
        }
        else if (header.name.toLowerCase() === "accept-charset") {
        }
        else if (header.name.toLowerCase() === "accept-encoding") {
        }
        else if (header.name.toLowerCase() === "accept-language") {
            header.value = getAcceptLanguageHeader();
        }
        else if (header.name.toLowerCase() === "authorization") {
        }
        else if (header.name.toLowerCase() === "expect") {
        }
        else if (header.name.toLowerCase() === "from") {
        }
        else if (header.name.toLowerCase() === "host") {
        }
        else if (header.name.toLowerCase() === "if-match") {
        }
        else if (header.name.toLowerCase() === "if-modified-since") {
        }
        else if (header.name.toLowerCase() === "if-none-match") {
        }
        else if (header.name.toLowerCase() === "if-range") {
        }
        else if (header.name.toLowerCase() === "if-unmodified-since") {
        }
        else if (header.name.toLowerCase() === "max-forwards") {
        }
        else if (header.name.toLowerCase() === "proxy-authorization") {
        }
        else if (header.name.toLowerCase() === "range") {
        }
        else if (header.name.toLowerCase() === "referer") {
            header.value = getRefererHeader();
        }
        else if (header.name.toLowerCase() === "te") {
        }
        else if (header.name.toLowerCase() === "user-agent") {
            header.value = getUserAgentHeader(seed);
        }
    }

    return {requestHeaders: e.requestHeaders};
}

chrome.webRequest.onBeforeSendHeaders.addListener(rewriteHttpHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);

chrome.webNavigation.onBeforeNavigate.addListener(function(details) {
    // Create URL object from url string
    var serverUrl = new URL(details.url);

    // Get the origin (hostname)
    var origin = serverUrl.hostname;

    var seed = "Some dynamic value";

    console.log("Injecting Value");
    chrome.tabs.executeScript(details.tabId, {code: "var seed = '" + seed + "';console.log(seed);", runAt: "document_start"}, function() {
        console.log("Value Injected");
    });
});

inject.js

(function() {
  function inject(filePath) {
    var script = document.createElement('script');
    script.src = chrome.extension.getURL(filePath);
    script.onload = function() {
      this.remove();
    };
    (document.head || document.documentElement).appendChild(script);
  }

  function injectText(text) {
    var script = document.createElement('script');
    script.textContent = text;
    script.onload = function() {
      this.remove();
    };
    (document.head || document.documentElement).appendChild(script);
  }

  console.log("CONTENT SCRIPT RUNNING");

  console.log(seed); // SEED IS NOT DEFINED HERE ???

  injectText("var seed = 'hello';");
  console.log("[INFO] Injected Seed ...");
  inject("js/ua.js");
  console.log("[INFO] Injected UA ...");
  inject("js/words.js");
  console.log("[INFO] Injected Words ...");
  inject("js/lib/seedrandom.min.js");
  console.log("[INFO] Injected Seed Random ...");
  inject("js/random.js");
  console.log("[INFO] Injected Random ...");
  inject("js/api/document.js");
  console.log("[INFO] Injected Document API ...");
  inject("js/api/navigator.js");
  console.log("[INFO] Injected Navigator API ...");
  inject("js/api/canvas.js");
  console.log("[INFO] Injected Canvas API ...");
  inject("js/api/history.js");
  console.log("[INFO] Injected History API ...");
  inject("js/api/battery.js");
  console.log("[INFO] Injected Battery API ...");
  inject("js/api/audio.js");
  console.log("[INFO] Injected Audio API ...");
  inject("js/api/element.js");
  console.log("[INFO] Injected Element API ...");
})();

I get the error when trying to log the seed to the console:

inject.js:26 Uncaught ReferenceError: seed is not defined
    at inject.js:26
    at inject.js:52

Any ideas?

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

This is going to be very tricky.

Let's look at your requirements.

Inject.js will need to have access to this variable and run it's code BEFORE any scripts on the page run.

That's not how your code currently works. Your inject.js is executed at document_end - which happens after the whole DOM tree is parsed, which means after all page scripts have run (barring asynchronous parts and async script loading).

Chrome has a solution to that - you can set your execution to document_start. Then your code will truly run before everything else, while DOM is still not parsed (so document is essentially empty). With what your code does, it should not create problems (it only relies on document.documentElement, which will exist).

Problem is, all your code has to be synchronous to still enjoy "runs before everything else" property. Chrome will pause DOM parsing as long as the synchronous part of your code runs, but then all bets are off as it merrily continues to parse (and run code from) the document.

This, for example, disqualifies chrome.storage and Messaging as access to that is necessarily asynchronous.

I need to inject a dynamic variable [on a page load]

Meaning that you cannot store this in advance in some synchronously-available storage (e.g. in localStorage or cookies of the website), which would be problematic anyway considering you don't know domains in advance.

Note, for your code in particular, this may not be that much of a factor; your "dynamic" value is in fact fixed per domain. You still don't know in advance which domain will be visited, but you can at least guarantee that on a second load it will be there.

Using my background script background.js, I need to inject a dynamic variable as a content script before injecting another file [that still needs to run before everything else on the page]

That's the tricky part. In fact, as stated, it's simply impossible. You're trying to catch, from the background, the exact moment between the navigation being committed, so that Chrome switched the page to the new domain, and the execution of your document_start script.

There is no detectable gap there, and no way to tell Chrome to wait. It's a race condition you have no hopes to resolve.

You're trying to use webNavigation.onBeforeNavigate - before even the navigation is committed. So your injectScript probably goes to the previous page even, rendering it useless. If you try some other event, e.g . onCommitted, there's still no telling as to when exactly injectScript will be treated. Likely after your script.

So, how to work around all this?

Fortunately, there is some synchronous storage that's available to the content script that you can push some information to right before the earliest of scripts executes.

Cookies.

However, using the chrome.cookies API won't help. You need to actively inject the cookie value into the request on webRequest.onHeadersReceived.

You have to have the value ready synchronously to process it with a blocking handler to onHeadersReceived, but then you can simply add one Set-Cookie header and have it immediately available in document.cookies in your inject.js.

  • background.js

    function addSeedCookie(details) {
      seed = SomethingSynchronous();
      details.responseHeaders.push({
        name: "Set-Cookie",
        value: `seed_goes_here=${seed};`
      });
      return {
        responseHeaders: details.responseHeaders
      };
    }
    
    chrome.webRequest.onHeadersReceived.addListener(
      addSeedCookie, {urls: ["<all_urls>"]}, [
        "blocking",
        "responseHeaders",
        // Chrome 72+ requires 'extraHeaders' to handle Set-Cookie header
        chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
      ].filter(Boolean)
    );
    
  • inject.js

    function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
      return document.cookie.split(';').reduce(function(prev, c) {
        var arr = c.split('=');
        return (arr[0].trim() === cookie) ? arr[1] : prev;
      }, undefined);
    }
    
    var seed = getCookie("seed_goes_here");
    

If an asynchronous function is needed to produce the data, prepare the data before the request is sent in onBeforeRequest event, then use it in onHeadersReceived listener.

const preparedSeed = {};
chrome.webRequest.onBeforeRequest.addListener(
  details => {
    chrome.storage.local.get('seed', data => {
      preparedSeed[details.requestId] = data.seed;
    });
  }, {
    urls: ['<all_urls>'],
    types: ['main_frame', 'sub_frame'],
  });

Note: the above code is untested and only serves to illustrate the idea.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...