add Patch CSP option
This commit is contained in:
parent
497f31e3cd
commit
0816933932
|
@ -976,6 +976,12 @@
|
||||||
"optionsAdvancedNewStyleAsUsercss": {
|
"optionsAdvancedNewStyleAsUsercss": {
|
||||||
"message": "Write new style as usercss"
|
"message": "Write new style as usercss"
|
||||||
},
|
},
|
||||||
|
"optionsAdvancedPatchCsp": {
|
||||||
|
"message": "Patch <code>CSP</code> to allow style assets"
|
||||||
|
},
|
||||||
|
"optionsAdvancedPatchCspNote": {
|
||||||
|
"message": "Enable this if some of your styles fail to show an image/background/font on sites with a strict <code>Content-Security-Policy</code>.\n\nEnabling this will loosen CSP a bit by merging it with <code>img-src data: *; font-src data: *; style-src 'unsafe-inline'</code> which means you should accept the potential risk and/or regularly check the CSS code of your styles. Read about CSS-based attacks for more information.\n\nNote, this is not guaranteed to take effect if another installed extension modifies the network response first."
|
||||||
|
},
|
||||||
"optionsAdvancedStyleViaXhr": {
|
"optionsAdvancedStyleViaXhr": {
|
||||||
"message": "Instant inject mode"
|
"message": "Instant inject mode"
|
||||||
},
|
},
|
||||||
|
|
129
background/style-via-webrequest.js
Normal file
129
background/style-via-webrequest.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/* global API CHROME prefs */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
CHROME && (async () => {
|
||||||
|
const idCsp = 'patchCsp';
|
||||||
|
const idOff = 'disableAll';
|
||||||
|
const idXhr = 'styleViaXhr';
|
||||||
|
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
|
||||||
|
const stylesToPass = {};
|
||||||
|
const enabled = {};
|
||||||
|
|
||||||
|
await prefs.initializing;
|
||||||
|
prefs.subscribe([idXhr, idOff, idCsp], toggle, {now: true});
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
const csp = prefs.get(idCsp) && !prefs.get(idOff);
|
||||||
|
const xhr = prefs.get(idXhr) && !prefs.get(idOff) && Boolean(chrome.declarativeContent);
|
||||||
|
if (xhr === enabled.xhr && csp === enabled.csp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Need to unregister first so that the optional EXTRA_HEADERS is properly registered
|
||||||
|
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
|
||||||
|
chrome.webRequest.onHeadersReceived.removeListener(modifyHeaders);
|
||||||
|
if (xhr || csp) {
|
||||||
|
const reqFilter = {
|
||||||
|
urls: ['<all_urls>'],
|
||||||
|
types: ['main_frame', 'sub_frame'],
|
||||||
|
};
|
||||||
|
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
|
||||||
|
chrome.webRequest.onHeadersReceived.addListener(modifyHeaders, reqFilter, [
|
||||||
|
'blocking',
|
||||||
|
'responseHeaders',
|
||||||
|
xhr && chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
|
||||||
|
].filter(Boolean));
|
||||||
|
}
|
||||||
|
if (enabled.xhr !== xhr) {
|
||||||
|
enabled.xhr = xhr;
|
||||||
|
toggleEarlyInjection();
|
||||||
|
}
|
||||||
|
enabled.csp = csp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs content scripts earlier than document_start */
|
||||||
|
function toggleEarlyInjection() {
|
||||||
|
const api = chrome.declarativeContent;
|
||||||
|
if (!api) return;
|
||||||
|
api.onPageChanged.removeRules([idXhr], async () => {
|
||||||
|
if (enabled.xhr) {
|
||||||
|
api.onPageChanged.addRules([{
|
||||||
|
id: idXhr,
|
||||||
|
conditions: [
|
||||||
|
new api.PageStateMatcher({
|
||||||
|
pageUrl: {urlContains: '://'},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
new api.RequestContentScript({
|
||||||
|
js: chrome.runtime.getManifest().content_scripts[0].js,
|
||||||
|
allFrames: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
|
||||||
|
function prepareStyles(req) {
|
||||||
|
API.getSectionsByUrl(req.url).then(sections => {
|
||||||
|
if (Object.keys(sections).length) {
|
||||||
|
stylesToPass[req.requestId] = !enabled.xhr ? true :
|
||||||
|
URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length);
|
||||||
|
setTimeout(cleanUp, 600e3, req.requestId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
|
||||||
|
function modifyHeaders(req) {
|
||||||
|
const {responseHeaders} = req;
|
||||||
|
const csp = responseHeaders.find(h => h.name.toLowerCase() === 'content-security-policy');
|
||||||
|
const id = stylesToPass[req.requestId];
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let res;
|
||||||
|
if (enabled.xhr) {
|
||||||
|
res = true;
|
||||||
|
responseHeaders.push({
|
||||||
|
name: 'Set-Cookie',
|
||||||
|
value: `${chrome.runtime.id}=${prefs.get(idOff) ? 1 : 0}${id}`,
|
||||||
|
});
|
||||||
|
// Allow cookies in CSP sandbox (known case: raw github urls)
|
||||||
|
if (csp) {
|
||||||
|
csp.value = csp.value.replace(/(?:^|;)\s*sandbox(\s+[^;]*|)(?=;|$)/, (s, allow) =>
|
||||||
|
allow.split(/\s+/).includes('allow-same-origin') ? s : `${s} allow-same-origin`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enabled.csp && csp) {
|
||||||
|
res = true;
|
||||||
|
const src = {};
|
||||||
|
for (let p of csp.value.split(';')) {
|
||||||
|
p = p.trim().split(/\s+/);
|
||||||
|
src[p[0]] = p.slice(1);
|
||||||
|
}
|
||||||
|
addToCsp(src, 'img-src', 'data:', '*');
|
||||||
|
addToCsp(src, 'font-src', 'data:', '*');
|
||||||
|
addToCsp(src, 'style-src', "'unsafe-inline'");
|
||||||
|
csp.value = Object.entries(src).map(([k, v]) => `${k} ${v.join(' ')}`).join('; ');
|
||||||
|
}
|
||||||
|
if (res) {
|
||||||
|
return {responseHeaders};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToCsp(src, name, ...values) {
|
||||||
|
const list = src[name] || (src[name] = []);
|
||||||
|
const def = src['default-src'] || [];
|
||||||
|
list.push(...values.filter(v => !list.includes(v) && !def.includes(v)));
|
||||||
|
if (!list.length) delete src[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUp(key) {
|
||||||
|
const blobId = stylesToPass[key];
|
||||||
|
delete stylesToPass[key];
|
||||||
|
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,98 +0,0 @@
|
||||||
/* global API CHROME prefs */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
CHROME && (async () => {
|
|
||||||
const prefId = 'styleViaXhr';
|
|
||||||
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
|
|
||||||
const stylesToPass = {};
|
|
||||||
let enabled;
|
|
||||||
|
|
||||||
await prefs.initializing;
|
|
||||||
prefs.subscribe([prefId, 'disableAll'], toggle, {now: true});
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
let value = prefs.get(prefId) && !prefs.get('disableAll');
|
|
||||||
if (!chrome.declarativeContent) { // not yet granted in options page
|
|
||||||
value = false;
|
|
||||||
}
|
|
||||||
if (value === enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
enabled = value;
|
|
||||||
if (value) {
|
|
||||||
const reqFilter = {
|
|
||||||
urls: ['<all_urls>'],
|
|
||||||
types: ['main_frame', 'sub_frame'],
|
|
||||||
};
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(passStyles, reqFilter, [
|
|
||||||
'blocking',
|
|
||||||
'responseHeaders',
|
|
||||||
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
|
|
||||||
].filter(Boolean));
|
|
||||||
} else {
|
|
||||||
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
|
|
||||||
chrome.webRequest.onHeadersReceived.removeListener(passStyles);
|
|
||||||
}
|
|
||||||
if (!chrome.declarativeContent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.declarativeContent.onPageChanged.removeRules([prefId], async () => {
|
|
||||||
if (!value) return;
|
|
||||||
chrome.declarativeContent.onPageChanged.addRules([{
|
|
||||||
id: prefId,
|
|
||||||
conditions: [
|
|
||||||
new chrome.declarativeContent.PageStateMatcher({
|
|
||||||
pageUrl: {urlContains: ':'},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
new chrome.declarativeContent.RequestContentScript({
|
|
||||||
allFrames: true,
|
|
||||||
// This runs earlier than document_start
|
|
||||||
js: chrome.runtime.getManifest().content_scripts[0].js,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
|
|
||||||
function prepareStyles(req) {
|
|
||||||
API.getSectionsByUrl(req.url).then(sections => {
|
|
||||||
const str = JSON.stringify(sections);
|
|
||||||
if (str !== '{}') {
|
|
||||||
stylesToPass[req.requestId] = URL.createObjectURL(new Blob([str])).slice(blobUrlPrefix.length);
|
|
||||||
setTimeout(cleanUp, 600e3, req.requestId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
|
|
||||||
function passStyles(req) {
|
|
||||||
const blobId = stylesToPass[req.requestId];
|
|
||||||
if (blobId) {
|
|
||||||
const {responseHeaders} = req;
|
|
||||||
responseHeaders.push({
|
|
||||||
name: 'Set-Cookie',
|
|
||||||
value: `${chrome.runtime.id}=${prefs.get('disableAll') ? 1 : 0}${blobId}`,
|
|
||||||
});
|
|
||||||
// allow cookies for sandbox CSP (known case: raw github urls)
|
|
||||||
for (const h of responseHeaders) {
|
|
||||||
if (h.name.toLowerCase() === 'content-security-policy' && h.value.includes('sandbox')) {
|
|
||||||
h.value = h.value.replace(/(?:^|;)\s*sandbox(\s+[^;]*|)(?=;|$)/, (s, allow) =>
|
|
||||||
allow.split(/\s+/).includes('allow-same-origin') ? s : `${s} allow-same-origin`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {responseHeaders};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanUp(key) {
|
|
||||||
const blobId = stylesToPass[key];
|
|
||||||
delete stylesToPass[key];
|
|
||||||
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -15,6 +15,7 @@ window.INJECTED !== 1 && (() => {
|
||||||
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
||||||
'newStyleAsUsercss': false, // create new style in usercss format
|
'newStyleAsUsercss': false, // create new style in usercss format
|
||||||
'styleViaXhr': false, // early style injection to avoid FOUC
|
'styleViaXhr': false, // early style injection to avoid FOUC
|
||||||
|
'patchCsp': false, // add data: and popular image hosting sites to strict CSP
|
||||||
|
|
||||||
// checkbox in style config dialog
|
// checkbox in style config dialog
|
||||||
'config.autosave': true,
|
'config.autosave': true,
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
"background/usercss-helper.js",
|
"background/usercss-helper.js",
|
||||||
"background/usercss-install-helper.js",
|
"background/usercss-install-helper.js",
|
||||||
"background/style-via-api.js",
|
"background/style-via-api.js",
|
||||||
"background/style-via-xhr.js",
|
"background/style-via-webrequest.js",
|
||||||
"background/search-db.js",
|
"background/search-db.js",
|
||||||
"background/update.js",
|
"background/update.js",
|
||||||
"background/openusercss-api.js"
|
"background/openusercss-api.js"
|
||||||
|
|
13
options.html
13
options.html
|
@ -247,6 +247,19 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>
|
||||||
|
<span i18n-html="optionsAdvancedPatchCsp"></span>
|
||||||
|
<a i18n-title="optionsAdvancedPatchCspNote"
|
||||||
|
data-cmd="note" href="#" class="svg-inline-wrapper">
|
||||||
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="onoffswitch">
|
||||||
|
<input type="checkbox" id="patchCsp" class="slider">
|
||||||
|
<span></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span i18n-text="optionsAdvancedExposeIframes">
|
<span i18n-text="optionsAdvancedExposeIframes">
|
||||||
<a i18n-title="optionsAdvancedExposeIframesNote"
|
<a i18n-title="optionsAdvancedExposeIframesNote"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user