Browser extension - Secure http requests from content scripts
Quentin Ménoret
Posted on February 5, 2021
I just published a new extension on Chrome and Firefox that allows anyone to run Code Tours from the Github UI. More information about Code Tours and the extension in this blog post.
Article No Longer Available
I thought it would be nice to write a series about how you could do exactly the same, step by step.
This third blog post will focus on sending cross origin http requests from a Content script.
The problem
Remember that code tour extension I was mentioning just before? Let's try to implement the first bit. What we want is to be able to retrieve the content of code tours (json files) so we can process them. You’d think it should be as simple as using fetch
with the right URL, right? Well, it’s a bit more complicated than that.
A naive approach
First, we need to find all code tours definition files. Once we’re on the .tours directory in Github, we can use a selector:
// Wait for the page to be ready
document.addEventListener("DOMContentLoaded", function(){
Array.from(
// Find all links to tour files
document.querySelectorAll('div[role=row] > div[role="rowheader"] > span > a').values(),
).map(
async (parentElement) => {
const title = parentElement.getAttribute('title')
const href = parentElement.getAttribute('href')
// Now we want to query the file content as a raw string.
// In Github, this means fetching the file using “raw” instead of “blob”
const codeTourUrl = href.replace('blob', 'raw')
// A code tour is a json object, we can use the fetch API to receive an object
const content = await fetch(codeTourUrl).then((response) => response.json())
console.log(title, content)
})
})
Copy this code in the Content Script file you've created in this post:
If you refresh the extension, it will get executed in any page you load. Go to the .tours folder of a project and open the console. You will see either one of these:
- On Chrome, the list of Code Tours and their contents will be loaded
- On Firefox, you will see an error and the http request will be denied
Forwarding the requests to the background
With this code, we should be able to retrieve all the code tours definition files. Unfortunately, Github will redirect us during this request. It won’t work on Firefox as this is a Cross Origin Request.
In general, you shouldn’t use fetch from your content script. The right way to process this will be to forward the query to the background script, so it can perform the request for you:
Let’s create a function allowing us to forward requests to the background script:
function forwardRequest(message) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (!response) return reject(chrome.runtime.lastError)
return resolve(response)
})
})
}
In the background script, we need to handle this request:
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// sendResponse can be used to send back a result to the content script
fetch(`https://github.com/${request.url}`)
.then((response) => response.json())
.then((codeTourContent) => sendResponse(codeTourContent))
// As we will reply asynchronously to the request, we need to tell chrome to wait for our response
return true
})
⚠️ Be careful with how you forward requests to your background script. You should make sure you won’t trigger requests to random URLs as this is fully authenticated. In this case we should verify the URL (make sure it’s safe) and only access GET requests. As you can see here we enforce the usage of the Github domain, but you should probably include more checks. If possible, build the URL yourself in the background script to be surer to target the exact resource you need.
Now that we have this code, we can replace the call to fetch in the background script with a call to forwardRequest
.
Array.from(
document.querySelectorAll('div[role=row] > div[role="rowheader"] > span > a').values(),
).map(
async (parentElement) => {
const title = parentElement.getAttribute('title')
const href = parentElement.getAttribute('href')
const codeTourUrl = href.replace('blob', 'raw')
// Now forward request will behave like fetch
const content = await forwardRequest({ url: codeTourUrl })
console.log(title, content)
})
This still won’t work on Firefox, as it will prevent the background script from making any requests to random hostnames. In order to fix this, you will need to ask the permission to query github.com
and render.githubusercontent.com
(github redirects you to this domain when querying a raw file) from the background script.
Just add this into your manifest file:
{
"permissions": ["https://render.githubusercontent.com/*", "https://github.com/*"]
}
Reload the extension, and it works!
Conclusion
We just deep dived into one really important aspect of Browser Extension: securely retrieving data from content script. In the next post, we’ll use the data to build a feature! Feel free to follow me here if you want to check the next one when it's out:
Photo by Ricardo Gomez Angel on Unsplash
Posted on February 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.