diff --git a/.config/docker_example.yml b/.config/docker_example.yml index f8124bc9df..e51d7e8f5f 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -144,3 +144,6 @@ signToActivityPubGet: true # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# Value of Content-Security-Policy header +#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';" diff --git a/.config/example.yml b/.config/example.yml index a19b5d04e8..d9f6238219 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -145,3 +145,6 @@ signToActivityPubGet: true # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# Value of Content-Security-Policy header +#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';" diff --git a/gulpfile.js b/gulpfile.js index a04ab4c1ad..d08b04eae7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,12 +36,18 @@ gulp.task('copy:frontend:locales', cb => { }); gulp.task('build:backend:script', () => { - return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) + const clientManifestExists = fs.existsSync('./built/_vite_/manifest.json'); + const clientEntry = clientManifestExists ? + JSON.parse(fs.readFileSync('./built/_vite_/manifest.json', 'utf-8'))['src/init.ts'].file + : 'src/init.ts' + + return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js', './packages/backend/src/server/web/flush.js']) .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) + .pipe(replace('CLIENT_ENTRY', JSON.stringify(clientEntry))) .pipe(terser({ toplevel: true })) - .pipe(gulp.dest('./packages/backend/built/server/web/')); + .pipe(gulp.dest('./built/_frontend_dist_/')); }); gulp.task('build:backend:style', () => { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index aa98ef1d22..4309f67cb3 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -48,6 +48,8 @@ export type Source = { allowedPrivateNetworks?: string[]; + contentSecurityPolicy?: string; + maxFileSize?: number; accesslog?: string; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index c69ee33ea3..7f1a43792f 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -172,6 +172,14 @@ export class ClientServerService { fastify.addHook('onRequest', (request, reply, done) => { // クリックジャッキング防止のためiFrameの中に入れられないようにする reply.header('X-Frame-Options', 'DENY'); + + // XSSが存在した場合に影響を軽減する + // (script-srcにunsafe-inline等を追加すると意味が無くなるので注意) + const csp = this.config.contentSecurityPolicy + ?? 'script-src \'self\' \'unsafe-eval\' ' + + 'https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; ' + + 'base-uri \'self\'; object-src \'self\';'; + reply.header('Content-Security-Policy', csp); done(); }); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index c6cb25e43a..4b9565f4c7 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -154,7 +154,7 @@

An error has occurred!

-

Don't worry, it's (probably) not your fault.

@@ -181,6 +181,9 @@
`; + document.getElementById("reload").addEventListener('click', () => { + location.reload(); + }); errorsElement = document.getElementById('errors'); } const detailsElement = document.createElement('details'); diff --git a/packages/backend/src/server/web/flush.js b/packages/backend/src/server/web/flush.js new file mode 100644 index 0000000000..bbc9356d00 --- /dev/null +++ b/packages/backend/src/server/web/flush.js @@ -0,0 +1,44 @@ +'use strict'; + +const msg = document.getElementById('msg'); +const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`; + +message('Start flushing.'); + +(async function () { + try { + localStorage.clear(); + message('localStorage cleared.'); + + const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { + const delidb = indexedDB.deleteDatabase(name); + delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); + delidb.onerror = e => rej(e) + })); + + await Promise.all(idbPromises); + + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + await navigator.serviceWorker.getRegistrations() + .then(registrations => { + return Promise.all(registrations.map(registration => registration.unregister())); + }) + .catch(e => { throw new Error(e) }); + } + + message(successText); + } catch (e) { + message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); + message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) + + console.error(e); + setTimeout(() => { + location = '/'; + }, 10000) + } +})(); + +function message(text) { + msg.insertAdjacentHTML('beforeend', `

[${(new Date()).toString()}] ${text.replace(/\n/g, '
')}

`) +} diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 8d6897c46d..5b3217aa95 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -63,12 +63,7 @@ html style include ../style.css - script. - var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{clientEntry.file}"; - - script - include ../boot.js + script(src=`/assets/boot.js?v=${version}`) body noscript: p diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug index 39a151a29b..54706b9c90 100644 --- a/packages/backend/src/server/web/views/bios.pug +++ b/packages/backend/src/server/web/views/bios.pug @@ -8,8 +8,7 @@ html title Misskey Repair Tool style include ../bios.css - script - include ../bios.js + script(src=`/assets/bios.js?v=${version}`) body header diff --git a/packages/backend/src/server/web/views/cli.pug b/packages/backend/src/server/web/views/cli.pug index d2cf7c4335..51d45cefd3 100644 --- a/packages/backend/src/server/web/views/cli.pug +++ b/packages/backend/src/server/web/views/cli.pug @@ -8,8 +8,7 @@ html title Misskey Cli style include ../cli.css - script - include ../cli.js + script(src=`/assets/cli.js?v=${version}`) body header diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug index a73a45212f..2ce426ae1d 100644 --- a/packages/backend/src/server/web/views/flush.pug +++ b/packages/backend/src/server/web/views/flush.pug @@ -2,46 +2,4 @@ doctype html html #msg - script. - const msg = document.getElementById('msg'); - const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`; - - message('Start flushing.'); - - (async function() { - try { - localStorage.clear(); - message('localStorage cleared.'); - - const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { - const delidb = indexedDB.deleteDatabase(name); - delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); - delidb.onerror = e => rej(e) - })); - - await Promise.all(idbPromises); - - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }) - .catch(e => { throw new Error(e) }); - } - - message(successText); - } catch (e) { - message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); - message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) - - console.error(e); - setTimeout(() => { - location = '/'; - }, 10000) - } - })(); - - function message(text) { - msg.insertAdjacentHTML('beforeend', `

[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}

`) - } + script(src=`/assets/flush.js?v=${version}`)