I'm working on an Edge extension, and I need to request JSON from an external website. The following example works fine on stackoverflow.com, but fails on steamcommunity.com/id/ where I need it.
Here are the files I have:
jQuery (from local file)
manifest.json:
{
"author": "me",
"name": "Get JSON",
"description": "get json",
"version": "0.1",
"manifest_version": 2,
"content_scripts": [
{
"matches": [
"*://steamcommunity.com/id/*",
"*://stackoverflow.com/*"
],
"js": ["jquery.min.js", "myscript.js"]
}
]
}
myscript.js:
var url = 'https://raw.githubusercontent.com/bahamas10/css-color-names/master/css-color-names.json';
$.getJSON(url, function(data) {
console.log(data);
});
As I've said, this example works fine on Stackoverflow, but fails on Steam.
This is the error I receive on the Steam website:
CSP14312: Resource violated directive 'connect-src 'self' http://steamcommunity.com https://steamcommunity.com https://api.steampowered.com/ http://localhost:27060 http://store.steampowered.com/ https://store.steampowered.com/' in Content-Security-Policy: https://raw.githubusercontent.com/bahamas10/css-color-names/master/css-color-names.json. Resource will be blocked.
EDIT: I should also add, that this exact example works fine when using a Chrome extension in Google Chrome.
Any help is appreciated.
Microsoft Edge, differently from Chrome and Firefox, doesn't ignore the website CSP rules for extensions.
See https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11320214/ for an open issue.
The only viable workaround I know, which I also tested myself, is to change the CSP response headers on the fly by using browser.webRequest.onHeadersReceived, which luckily is well supported in Edge.
NOTE: this code was adapted from this great article, which also explains the problem in more detail: https://transitory.technology/browser-extensions-and-csp-headers/
The code adds your extension to the allowed urls for each CSP header and directive, it should be executed in the background page, which should be set as persistent.
Make sure to add the webRequest and webRequestBlocking permissions to your manifest.json.
const cspHeaderNames = [
'content-security-policy',
'content-security-policy-report-only',
'x-webkit-csp'
];
// #see https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives
const cspSources = [
'connect-src',
'default-src',
'font-src',
'frame-src',
'img-src',
'media-src',
'object-src',
'script-src',
'style-src',
'child-src',
'form-action',
];
const url = browser.extension.getURL("").slice(0, -1); // (remove trailing "/")
browser.webRequest.onHeadersReceived.addListener(details => {
details.responseHeaders.forEach(header => {
const isCspHeader = cspHeaderNames.indexOf(header.name.toLowerCase()) >= 0;
if (isCspHeader) {
let newValue = header.value;
cspSources.forEach(source => {
newValue = newValue.replace(source, source + ' ' + url);
});
header.value = newValue;
}
});
return {
responseHeaders: details.responseHeaders
};
}, {
urls: ['<all_urls>'],
types: ['main_frame', "sub_frame"],
}, ["blocking", "responseHeaders"]);
Related
I'm trying to create a simple extension for Chrome and Firefox which just gets some content from the DOM of certain pages and adds other content.
But the issue is that it only works the first time I load it as a temporary Add-on for testing on both Chrome and Firefox, or when I hit the extension's reload button on about:debugging on Firefox.
My manifest.json only contains the following information:
{
"manifest_version": 2,
"name": "ft_blackhole",
"version": "0.1",
"description": "Shows how many days you have left before you get absorbed by the Blackhole.",
"icons": {
"48": "icons/blackhole.png"
},
"content_scripts": [
{
"matches": [
"https://profile.intra.42.fr/",
"https://profile.intra.42.fr/users/*",
"https://profile.intra.42.fr/users/*/"
],
"js": [
"ft_blackhole.js"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "ft_blackhole-0.1#intra.42.fr"
}
}
}
I use almost the same exact manifest file (same exact matches) for another extension that works and depends on the same URLs. That extension works fine. The difference between it and this one is that while they both insert content in the DOM, my older extension does it after it fetches data from an API, but this one just gets some text from the current page's DOM:
ft_blackhole.js:
console.log("Hello World");
let blackholeDiv = document
.getElementById("bh")
.getElementsByClassName("emote-bh")[0];
let daysLeft = blackholeDiv.getAttribute("data-original-title");
let daysNum = daysLeft.split(" ")[0];
let status = (() => {
if (daysNum <= 14)
return {cat: "😿", color: "#D8636F"};
else if (daysNum <= 42)
return {cat: "🙀", color: "#F0AD4E"};
else
return {cat: "😸", color: "#5CB85C"};
})();
let daysLeftDiv = document.createElement("div");
daysLeftDiv.innerText = daysLeft + ' ' + status["cat"];
daysLeftDiv.style.color = status["color"];
daysLeftDiv.style.fontSize = "0.7em";
daysLeftDiv.style.fontWeight = "400";
blackholeDiv
.children[1]
.appendChild(daysLeftDiv);
I searched all over the internet, I couldn't understand what is causing it to run only the first time the extension is installed, but then when I refresh the page, it doesn't add anything to the page anymore, until I reload the extension again from about:debugging.
I hope someone could help.
Edit:
I also noticed that it also works after a hard reload of the page on Chrome, but not after a normal reload.
Edit 2:
When I console.log() something in the content script, it works all the time, always logs when I reload the page (normal reload), but the other code for DOM manipulation doesn't...
Edit 3:
I have uploaded a static copy of the profile page if anyone wants to take a look at the HTML content: https://haddi.me/intra-example/intra.html
Edit 4:
It seems the issue is caused after everything is loaded in the DOM, by some javascript that runs after that and makes changes to it, I used Mutation Observer on the target node to log those changes, and there were indeed some few ones, what I can't figure out is how am I supposed to run my code only after that last change (Which is their modification of span#bh-date's style attribute)? This the code I added:
const blackholeDiv = document
.getElementById("bh")
.getElementsByClassName("emote-bh")[0];
// Callback function to execute when mutations are observed
• const callback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed: ', mutation);
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified: ', mutation);
}
}
};
const config = {attributes: true, childList: true, subtree: true};
const observer = new MutationObserver(callback);
observer.observe(blackholeDiv, config);
And these are the logs:
Screenshot from Chrome's Devtools Console
In your manifest.json file, I do not see a "run_at", to make sure the DOM is ready and then immediately inject the script. Use the "document_end" string in your code.
Source:
https://developer.chrome.com/docs/extensions/mv3/content_scripts/#run_time
manifest.json
{
"name": "Test Extenstion",
"description": "Content test!",
"version": "1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["https://haddi.me/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"permissions": ["activeTab"]
}
Tested in Google Chrome web browser version 99.0.4844.83.
I'm the author of Intab, a Chrome extension that lets you view a link inline as opposed to a new tab. There's not much fancy stuff going on behind the scenes, it's just an iframe that loads the URL the user clicked on.
It works great except for sites that set the X-Frame-Options header to DENY or SAMEORIGIN. Some really big sites like Google and Facebook both use it which makes for a slightly janky experience.
Is there any way to get around this? Since I'm using a Chrome extension, is there any browser level stuff I can access that might help? Looking for any ideas or help!
Chrome offers the webRequest API to intercept and modify HTTP requests. You can remove the X-Frame-Options header to allow inlining pages within an iframe.
chrome.webRequest.onHeadersReceived.addListener(
function(info) {
var headers = info.responseHeaders;
for (var i=headers.length-1; i>=0; --i) {
var header = headers[i].name.toLowerCase();
if (header == 'x-frame-options' || header == 'frame-options') {
headers.splice(i, 1); // Remove header
}
}
return {responseHeaders: headers};
}, {
urls: [
'*://*/*', // Pattern to match all http(s) pages
// '*://*.example.org/*', // Pattern to match one http(s) site
],
types: [ 'sub_frame' ]
}, [
'blocking',
'responseHeaders',
// Modern Chrome needs 'extraHeaders' to see and change this header,
// so the following code evaluates to 'extraHeaders' only in modern Chrome.
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean)
);
In the manifest, you need to specify the webRequest and webRequestBlocking permissions, plus the URLs patterns you're intending to intercept i.e. "*://*/*" or "*://www.example.org/*" for the example above.
ManifestV3 example using declarativeNetRequest
See also the warning at the end of this answer!
manifest.json for Chrome 96 and newer,
doesn't show a separate permission for "Block page content" during installation
"minimum_chrome_version": "96",
"permissions": ["declarativeNetRequestWithHostAccess"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 101 and newer using initiatorDomains and requestDomains
(don't forget to add "minimum_chrome_version": "101" in manifest.json)
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
const RULE = {
id: 1,
condition: {
initiatorDomains: [chrome.runtime.id],
requestDomains: iframeHosts,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
],
},
};
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [RULE.id],
addRules: [RULE],
});
});
Old Chrome 84-100
Use the following instead, if your extension should be compatible with these old versions.
manifest.json for Chrome 84 and newer,
shows a separate permission for "Block page content" during installation
"permissions": ["declarativeNetRequest"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 84 and newer using the now deprecated domains
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: iframeHosts.map((h, i) => i + 1),
addRules: iframeHosts.map((h, i) => ({
id: i + 1,
condition: {
domains: [chrome.runtime.id],
urlFilter: `||${h}/`,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
],
},
})),
});
});
Warning: beware of site's service worker
You may have to remove the service worker of the site(s) and clear its cache before adding the iframe or before opening the extension page because many modern sites use the service worker to create the page without making a network request thus ignoring our header-stripping rule.
Add "browsingData" to "permissions" in manifest.json
Clear the SW:
function removeSW(url) {
return chrome.browsingData.remove({
origins: [new URL(url).origin],
}, {
cacheStorage: true,
serviceWorkers: true,
});
}
// If you add an iframe element in DOM:
async function addIframe(url, parent = document.body) {
await removeSW(url);
const el = document.createElement('iframe');
parent.appendChild(el);
el.src = url;
return el;
}
// If you open an extension page with an <iframe> element in its HTML:
async function openPage(url) {
await removeSW('https://example.com/');
return chrome.tabs.create({url});
}
You can try the Frame extension that lets the user drop X-Frame-Options and Content-Security-Policy HTTP response headers, allowing pages to be iframed.
The code is available on github
It's based on ManifestV3 and working perfectly with Google & Facebook.
I'm the author of Intab, a Chrome extension that lets you view a link inline as opposed to a new tab. There's not much fancy stuff going on behind the scenes, it's just an iframe that loads the URL the user clicked on.
It works great except for sites that set the X-Frame-Options header to DENY or SAMEORIGIN. Some really big sites like Google and Facebook both use it which makes for a slightly janky experience.
Is there any way to get around this? Since I'm using a Chrome extension, is there any browser level stuff I can access that might help? Looking for any ideas or help!
Chrome offers the webRequest API to intercept and modify HTTP requests. You can remove the X-Frame-Options header to allow inlining pages within an iframe.
chrome.webRequest.onHeadersReceived.addListener(
function(info) {
var headers = info.responseHeaders;
for (var i=headers.length-1; i>=0; --i) {
var header = headers[i].name.toLowerCase();
if (header == 'x-frame-options' || header == 'frame-options') {
headers.splice(i, 1); // Remove header
}
}
return {responseHeaders: headers};
}, {
urls: [
'*://*/*', // Pattern to match all http(s) pages
// '*://*.example.org/*', // Pattern to match one http(s) site
],
types: [ 'sub_frame' ]
}, [
'blocking',
'responseHeaders',
// Modern Chrome needs 'extraHeaders' to see and change this header,
// so the following code evaluates to 'extraHeaders' only in modern Chrome.
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean)
);
In the manifest, you need to specify the webRequest and webRequestBlocking permissions, plus the URLs patterns you're intending to intercept i.e. "*://*/*" or "*://www.example.org/*" for the example above.
ManifestV3 example using declarativeNetRequest
See also the warning at the end of this answer!
manifest.json for Chrome 96 and newer,
doesn't show a separate permission for "Block page content" during installation
"minimum_chrome_version": "96",
"permissions": ["declarativeNetRequestWithHostAccess"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 101 and newer using initiatorDomains and requestDomains
(don't forget to add "minimum_chrome_version": "101" in manifest.json)
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
const RULE = {
id: 1,
condition: {
initiatorDomains: [chrome.runtime.id],
requestDomains: iframeHosts,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
],
},
};
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [RULE.id],
addRules: [RULE],
});
});
Old Chrome 84-100
Use the following instead, if your extension should be compatible with these old versions.
manifest.json for Chrome 84 and newer,
shows a separate permission for "Block page content" during installation
"permissions": ["declarativeNetRequest"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 84 and newer using the now deprecated domains
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: iframeHosts.map((h, i) => i + 1),
addRules: iframeHosts.map((h, i) => ({
id: i + 1,
condition: {
domains: [chrome.runtime.id],
urlFilter: `||${h}/`,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
],
},
})),
});
});
Warning: beware of site's service worker
You may have to remove the service worker of the site(s) and clear its cache before adding the iframe or before opening the extension page because many modern sites use the service worker to create the page without making a network request thus ignoring our header-stripping rule.
Add "browsingData" to "permissions" in manifest.json
Clear the SW:
function removeSW(url) {
return chrome.browsingData.remove({
origins: [new URL(url).origin],
}, {
cacheStorage: true,
serviceWorkers: true,
});
}
// If you add an iframe element in DOM:
async function addIframe(url, parent = document.body) {
await removeSW(url);
const el = document.createElement('iframe');
parent.appendChild(el);
el.src = url;
return el;
}
// If you open an extension page with an <iframe> element in its HTML:
async function openPage(url) {
await removeSW('https://example.com/');
return chrome.tabs.create({url});
}
You can try the Frame extension that lets the user drop X-Frame-Options and Content-Security-Policy HTTP response headers, allowing pages to be iframed.
The code is available on github
It's based on ManifestV3 and working perfectly with Google & Facebook.
When i use this following script it works with normal browser. But when iframe is used then its showing me this error:
Does anyone know what is causing this and can be resolved?
ERROR:
channel message Object {type: "getScreenPending", id: 24504, request: 6} content.js:4
channel message Object {type: "gotScreen", id: 24504, request: 6} content.js:4
>>> ShareScreen: if any err NavigatorUserMediaError {constraintName: "", message: "", name: "InvalidStateError"} test.js:1616
manifest.json:
{
"name": "Screen sharing",
"description": "Screensharing utility",
"version": "0.0.2",
"manifest_version": 2,
"minimum_chrome_version": "34",
"icons": {
"48" : "icon.png"
},
"permissions": [
"desktopCapture"
],
"background": {
"scripts": ["background.js"]
},
"content_scripts": [ {
"js": [ "content.js" ],
"all_frames": true,
"run_at": "document_start",
"matches": ["*://*.a.com/*", "*://*.b.com/*"]
}],
"web_accessible_resources": [
"icon.png"
]
}
background.js:
/* background page, responsible for actually choosing media */
chrome.runtime.onConnect.addListener(function (channel) {
channel.onMessage.addListener(function (message) {
switch(message.type) {
case 'getScreen':
var pending = chrome.desktopCapture.chooseDesktopMedia(message.options || ['screen', 'window'],
channel.sender.tab, function (streamid) {
// communicate this string to the app so it can call getUserMedia with it
message.type = 'gotScreen';
message.sourceId = streamid;
channel.postMessage(message);
});
// let the app know that it can cancel the timeout
message.type = 'getScreenPending';
message.request = pending;
channel.postMessage(message);
break;
case 'cancelGetScreen':
chrome.desktopCapture.cancelChooseDesktopMedia(message.request);
message.type = 'canceledGetScreen';
channel.postMessage(message);
break;
}
});
});
content.js:
/* the chrome content script which can listen to the page dom events */
var channel = chrome.runtime.connect();
channel.onMessage.addListener(function (message) {
console.log('channel message', message);
window.postMessage(message, '*');
});
window.addEventListener('message', function (event) {
if (event.source != window)
return;
if (!event.data && (event.data.type == 'getScreen' || event.data.type == 'cancelGetScreen'))
return;
channel.postMessage(event.data);
});
This is caused by the fact that the a stream can only be used by frames whose URL match the origin of the tab. Starting with Chrome 40, you can use the stream in frames as well if you set tab.url to a URL whose origin matches the frame (crbug.com/425344).
The stream is only valid for ten seconds, so you have to follow the following flow:
Load the iframe that contains the page that should handle the stream. This page must be served from a secure scheme, e.g. https: or chrome-extension:.
Send the frame's origin (location.origin) to the background page.
Request the desktop stream using the tab info, with tab.url set to the frame's URL or origin.
Send the streamId back to the frame and use it (within ten seconds).
Example (based on the code in the question):
var tab = channel.sender.tab;
// NEW (Chrome 40+)
tab.url = message.url; // Your custom message, e.g. {url: location.origin}
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab,
function (streamid) {
// ... see question for the rest of the code
});
1 - this is not a code problem, browser problem
2 - this is not working because i am launching the extension from HTTP (http://www.maindomain.com) using iframe a HTTPS (https://subdomain.maindomain.com) link which is using the browser extension
So to fix it. I needed to use HTTPS (https://www.maindomain.com) opening HTTPS iframe links (https://subdomain.maindomain.com) . Since then it works now.
Hope this help others.
NOTE: problem occurred: when i run the iframe from same subdomain subdomain.maindomain.com/test.php (iframe src=subdomain.maindomain.com/core.php) then it works. But when i am running it as maindomain.com/otherpages (iframe src=subdomain.maindomain.com/core.php) then this is not working. Was very confusing.
EDIT: This still did not resolved the problem. screen share dialog box opens but when i press share screen then it gives same error and fails.
I have a Chrome extension that I'm working on that redirects users of a non-HTTPS site to the HTTPS version automatically.
However, the current problem with this is that the user must activate this redirection manually.
This would be an easy feat to accomplish using content_scripts in manifest.json, however, according to the Chrome documentation, content scripts "Cannot...Use chrome.* APIs (except for parts of chrome.extension)".
So, here's the manifest file for my extension:
{
"name": "SSL Redirect",
"version": "1.0",
"manifest_version": 2,
"description": "Redirects plain HTTP domain.com to the encrypted, HTTPS secured version.",
"permissions": [ "tabs", "http://*/*", "https://*/*" ],
"background" : {
"page": "body.html"
},
"browser_action": {
"default_icon": "icon.png"
},
"content_scripts": [
{
"matches": ["http://www.domain.com/*"],
"js": ["redirect.js"]
}
]
}
And here's the js:
var domain = /domain.com\//;
var ssldomain = "ssl.domain.com\/";
function updateUrl(tab){
if(tab.url.match(ssldomain)) {
alert("You're already using the SSL site. :)")
throw { name: 'Error', message: 'Stopped running, already in SSL mode.' };
}
if(tab.url.match(domain)) {
var newurl = tab.url.replace(domain, ssldomain);
newurl = newurl.replace(/^http:/, 'https:');
newurl = newurl.replace("www.", "");
chrome.tabs.update(tab.id, {url: newurl});
}
if(!(tab.url.match(domain))) {
alert("This extension only works on domain.com.")
throw { name: 'Error', message: 'Stopped running, not on domain.com.' };
}
}
chrome.browserAction.onClicked.addListener(function(tab) {updateUrl(tab);});
My ultimate goal is to get this to run automatically on any page matching domain.com, without user interaction.
I'm a little stuck. Any ideas?
1) From within the content script, you can use standard methods of changing URL, since you are running in the context of the page. I.e.:
var oldUrl = location.href;
/* construct newUrl */
if(newUrl != oldUrl) location.replace(newUrl);
2) Scrap what you've written already and read about chrome.webRequest API.
This will achieve what you need without a content script or tab manipulation.
Example:
chrome.webRequest.onBeforeRequest.addListener(
function(details) {
var url = details.url.replace(/^http/, "https");
return {redirectUrl: url};
},
{urls: ["http://domain.com/*"]},
["blocking"]
);
Note: you need host permissions for "*://domain.com/*"