google-chrome-extensioncontent-security-policychrome-extension-manifest-v3

Google Chrome extension that I'm working on keeps throwing CSP errors


I'm creating a Chrome extension exclusively for personal use. I need it to watch over any requests made to https://www.instagram.com/api/graphql and log the response (that is, forward it to another script that does that). I am very new to Chrome Extension development, so please forgive me any noob mistakes.

Currently it seems that I'm able to fetch the response; console.log also prints out the response fine. However, I'm stuck at the CSP errors (Content Security Policy), and it won't forward the response it gathered to my script (currently on localhost).

What I have so far, a mixture of copy-pasting stuff from all around, but I understand what it does. I just don't understand what I have to do to make it do what I want 😊😊:

manifest.json

{

    "manifest_version": 3,
    "name": "BytE IG",
    "description": "BytE Instagram checker",
    "version": "1.3",
    "content_security_policy": {
        "extension_pages": "script-src 'self' http://localhost; object-src 'self' script-src-elem 'self'"
    },

    "background": {
        "service_worker": "background.js"
    },

    "host_permissions": [
        "*://*/*"
    ],

    "permissions": [
        "activeTab",
        "tabs",
        "webRequest"
    ],

    "content_scripts": [
        {
            "matches": ["*://*.instagram.com/direct/*"],
            "run_at": "document_start",
            "js": ["inject.js"]
        }
    ],

    "web_accessible_resources": [
        {
            "resources": ["injected.js"],
            "matches": ["*://*.instagram.com/*"]
        }
    ]

}

background.js

chrome.webRequest.onCompleted.addListener(
    (details) => {
      const url = new URL(details.url);

      console.log('caught request' + url);
    },
    {
      urls: "https://*.instagram.com/api/graphql*"
    }
);

inject.js

var s = document.createElement('script');
s.src = chrome.runtime.getURL('injected.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

injected.js

(function(xhr) {

    var XHR = XMLHttpRequest.prototype;

    var open = XHR.open;
    var send = XHR.send;
    var setRequestHeader = XHR.setRequestHeader;

    XHR.open = function(method, url) {
        this._method = method;
        this._url = url;
        this._requestHeaders = {};
        this._startTime = (new Date()).toISOString();

        return open.apply(this, arguments);
    };

    XHR.setRequestHeader = function(header, value) {
        this._requestHeaders[header] = value;
        return setRequestHeader.apply(this, arguments);
    };

    XHR.send = function(postData) {

        this.addEventListener('load', function() {
            var endTime = (new Date()).toISOString();

            var myUrl = this._url ? this._url.toLowerCase() : this._url;
            if(myUrl) {

                if (postData) {
                    if (typeof postData === 'string') {
                        try {
                            // here you get the REQUEST HEADERS, in JSON format, so you can also use JSON.parse
                            this._requestHeaders = postData;    
                        } catch(err) {
                            console.log('Request Header JSON decode failed, transfer_encoding field could be base64');
                            console.log(err);
                        }
                    } else if (typeof postData === 'object' || typeof postData === 'array' || typeof postData === 'number' || typeof postData === 'boolean') {
                            // do something if you need
                    }
                }

                // here you get the RESPONSE HEADERS
                var responseHeaders = this.getAllResponseHeaders();

                if ( this.responseType != 'blob' && this.responseText) {
                    // responseText is string or null
                    try {

                        // here you get RESPONSE TEXT (BODY), in JSON format, so you can use JSON.parse
                        var arr = this.responseText;

                        // printing url, request headers, response headers, response body, to console

                        // building your form values
                        var data = new URLSearchParams();
                        data.set('ig', arr);

                        // send to the endpoint
                        fetch("http://localhost:1608/ig.php", {
                            method: 'POST',
                            mode: 'no-cors',
                            cache: 'no-cache',
                            headers: {
                                'Content-Type': 'application/x-www-form-urlencoded',
                            },
                            body: data
                        }).then(function(response) {
                            // check the response object for result
                            // ...
                        });

                    } catch(err) {
                        console.log("Error in responseType try catch");
                        console.log(err);
                    }
                }

            }
        });

        return send.apply(this, arguments);
    };

})(XMLHttpRequest);

Instead of getting a nice little JSON to ig.php, I get:

injected.js:62 Refused to connect to 'http://localhost:1608/ig.php' because it violates the document's Content Security Policy. (anonymous) @ injected.js:62 injected.js:62 Refused to connect to 'http://localhost:1608/ig.php' because it violates the document's Content Security Policy. (anonymous) @ injected.js:62 injected.js:62 Refused to connect to 'http://localhost:1608/ig.php' because it violates the document's Content Security Policy. (anonymous) @ injected.js:62 injected.js:62 Refused to connect to 'http://localhost:1608/ig.php' because it violates the document's Content Security Policy. (anonymous) @ injected.js:62 injected.js:62 Refused to connect to 'http://localhost:1608/ig.php' because it violates the document's Content Security Policy.

Edit:

I also tried using a FQDN instead of localhost, with an SSL certificate. No luck.


Solution

  • I've put this on hold for a while, and just got back to it. Thanks to the comment by wOxxOm I managed to make this usable, and also convert it to v3.

    This is way out of my area of expertise (SysAdmin, DevOps), so it definitely has many flaws, but I just need it to work on my machine. It probably has permission that is doesn't need, but this was just a really quick integration that I needed ASAP.

    This is the new code that finally works:

    manifest.json:

        {
            "manifest_version": 3,
            "name": "BytE IG",
            "description": "BytE Instagram checker",
            "version": "2.0",
            "background": {
                "service_worker": "background.js"
            },
            "host_permissions": [
                "*://*/*"
            ],
            "permissions": [
                "activeTab",
                "tabs",
                "webRequest"
            ],
        
            "externally_connectable": {
                "matches": [
                    "*://*.instagram.com/*"
                ]
            },
              
            "content_scripts": [
                {
                    "matches": [
                        "*://*.instagram.com/direct/*"
                    ],
                    "run_at": "document_start",
                    "js": [
                        "inject.js"
                    ]
                }
            ],
            "web_accessible_resources": [
                {
                    "resources": ["injected.js"],
                    "matches": ["*://*.instagram.com/*"]
                }
            ]
        }
    

    inject.js

    var s = document.createElement('script');
    s.src = chrome.runtime.getURL('injected.js');
    s.onload = function() {
        this.remove();
    };
    (document.head || document.documentElement).appendChild(s);json
    

    injected.js

        (function (xhr) {
        
            var XHR = XMLHttpRequest.prototype;
        
            var open = XHR.open;
            var send = XHR.send;
            var setRequestHeader = XHR.setRequestHeader;
        
            XHR.open = function (method, url) {
                this._method = method;
                this._url = url;
                this._requestHeaders = {};
                return open.apply(this, arguments);
            };
        
            XHR.setRequestHeader = function (header, value) {
                this._requestHeaders[header] = value;
                return setRequestHeader.apply(this, arguments);
            };
        
            XHR.send = function (postData) {
        
                this.addEventListener('load', function () {
                    var extensionId = "your-extension-id";
        
                    var myUrl = this._url ? this._url.toLowerCase() : this._url;
                    if (myUrl) {
        
                        if (postData) {
                            if (typeof postData === 'string') {
                                try {
                                    this._requestHeaders = postData;
                                } catch (err) {
                                    console.log(err);
                                }
                            }
                        }
        
                        var responseHeaders = this.getAllResponseHeaders();
        
                        if (this.responseType != 'blob' && this.responseText) {
                            // responseText is string or null
                            try {
                                var arr = this.responseText;
                                chrome.runtime.sendMessage(extensionId, {arr: arr});
                            } catch (err) {
                                console.log(err);
                            }
                        }
        
                    }
                });
        
                return send.apply(this, arguments);
            };
        
        })(XMLHttpRequest);
    

    background.js (service worker)

    chrome.runtime.onMessageExternal.addListener(
      function (request, sender, sendResponse) {
        const url = new URL(sender.url);
    
        console.log('caught request' + url);
        var data = new URLSearchParams();
        data.set('ig', request.arr);
    
        // send to the endpoint
        fetch("http://localhost:1609/ig.php", {
          method: 'POST',
          mode: 'no-cors',
          cache: 'no-cache',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          body: data
        }).then(function (response) {
          // check the response object for result
          // ...
        });
      });
    

    The inject.js ... injects ... injected.js onto the page. However, with CSP, you cannot directly interact with websites except the ones that were approved by the website owner. I have tried to circumvent that because I only need it to work on my browser, but no luck.

    However, you can send messages to your own extension from approved websites, which can then send them wherever it's needed.

    Nice. :)