diff --git a/dev/boot.js b/dev/boot.js index f8c9ac381..a60a14e66 100644 --- a/dev/boot.js +++ b/dev/boot.js @@ -16,12 +16,14 @@ const toggle(div); }, - loadScript = src => src ? new Promise((resolve, reject) => { + loadScript = (src, integrity) => src ? new Promise((resolve, reject) => { const script = doc.createElement('script'); script.onload = () => resolve(); script.onerror = () => reject('Failed loading ' + src); script.src = src; // script.async = true; +// script.crossorigin = ''; // same as 'anonymous' + if (integrity) { script.integrity = integrity; } doc.head.append(script); }) : Promise.reject('src is empty'); @@ -146,8 +148,8 @@ if (!navigator.cookieEnabled) { RL_APP_DATA = appData; const url = appData.StaticLibsJs, cb = () => rl.app.bootstart(); - loadScript(url) - .then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`))) + loadScript(url, appData.StaticLibsJsSri) + .then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`), appData.StaticAppJsSri)) .then(() => appData.PluginsLink ? loadScript(qUri(appData.PluginsLink)) : Promise.resolve()) .then(() => rl.app ? cb() diff --git a/gulpfile.js b/gulpfile.js index 9b0d1bbb4..4bfa5b8d7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,17 +5,19 @@ const { cleanStatic } = require('./tasks/common'); const { js, jsLint } = require('./tasks/js'); const { css, cssLint } = require('./tasks/css'); const { vendors } = require('./tasks/vendors'); +const { sri } = require('./tasks/sri'); const clean = gulp.series(cleanStatic); const lint = gulp.parallel(jsLint, cssLint); const buildState1 = gulp.parallel(js, css, vendors); -const buildState2 = gulp.series(clean, buildState1); +const buildState2 = gulp.series(clean, buildState1, sri); const build = gulp.parallel(lint, buildState2); exports.css = css; exports.lint = lint; +exports.sri = sri; exports.build = build; exports.default = build; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 0e39503f9..598170540 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -779,6 +779,12 @@ class Actions $aResult['StaticLibsJs'] = Utils::WebStaticPath('js/' . ($bAppJsDebug ? '' : 'min/') . 'libs' . ($bAppJsDebug ? '' : '.min') . '.js'); + if (!$bAppJsDebug) { + $aSri = \SnappyMail\Integrity::loadSriHashes(); + $aResult['StaticLibsJsSri'] = $aSri['libs.min.js'] ?? ''; + $aResult['StaticAppJsSri'] = $aSri[($bAdmin ? 'admin' : 'app') . '.min.js'] ?? ''; + } + $this->oPlugins->InitAppData($bAdmin, $aResult, $oAccount); return $aResult; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php index 52104cf80..461aef273 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php @@ -207,9 +207,12 @@ abstract class Service if ($sResult) { $sResult .= ''; } else { + $aSri = $sAppCssMin ? \SnappyMail\Integrity::loadSriHashes() : []; + $sCssFile = ($bAdmin ? 'admin' : 'app').$sAppCssMin.'.css'; $aTemplateParameters['{{BaseAppBootCss}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css'); $aTemplateParameters['{{BaseAppBootScript}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin.'.js'); - $aTemplateParameters['{{BaseAppMainCssLink}}'] = Utils::WebStaticPath('css/'.($bAdmin ? 'admin' : 'app').$sAppCssMin.'.css'); + $aTemplateParameters['{{BaseAppMainCssLink}}'] = Utils::WebStaticPath('css/'.$sCssFile); + $aTemplateParameters['{{BaseAppMainCssSri}}'] = empty($aSri[$sCssFile]) ? '' : ' integrity="'.$aSri[$sCssFile].'"'; $aTemplateParameters['{{BaseAppThemeCss}}'] = \preg_replace('/\\s*([:;{},]+)\\s*/s', '$1', $oActions->compileCss($sThemeName, $bAdmin)); $aTemplateParameters['{{BaseLanguage}}'] = $oActions->compileLanguage($sLanguage, $bAdmin); $aTemplateParameters['{{BaseTemplates}}'] = Utils::ClearHtmlOutput($oServiceActions->compileTemplates($bAdmin)); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php b/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php index 97873d50c..6f0ff3447 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php @@ -8,7 +8,7 @@ abstract class Integrity /** * Called by https://webmail.tld/?/Test */ - public static function test() + public static function test(): void { $result = static::phpVersion(); if ($result) { @@ -89,14 +89,15 @@ abstract class Integrity } } - public static function phpVersion() + public static function phpVersion(): ?string { if (PHP_VERSION_ID < 80200) { return 'Your PHP version ('.PHP_VERSION.') is lower than the minimal required 8.2.0!'; } + return null; } - public static function phpExtensions() + public static function phpExtensions(): array { $aRequirements = array( 'openssl' => extension_loaded('openssl'), @@ -123,4 +124,23 @@ abstract class Integrity return $aMissing; } + // https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity + private static $aSri; + public static function loadSriHashes(): array + { + if (!\is_array($aSri)) { + $sSriPath = APP_VERSION_ROOT_PATH . 'static/sri.json'; + $aSri = \is_readable($sSriPath) ? \json_decode(\file_get_contents($sSriPath), true) : []; + if (!$aSri) { + $aSri = [ + 'libs.min.js' => \hash_file('sha384', APP_VERSION_ROOT_PATH . 'static/js/min/libs.min.js'), + 'admin.min.js' => \hash_file('sha384', APP_VERSION_ROOT_PATH . 'static/js/min/admin.min.js'), + 'app.min.js' => \hash_file('sha384', APP_VERSION_ROOT_PATH . 'static/js/min/app.min.js'), + 'admin.min.css' => \hash_file('sha384', APP_VERSION_ROOT_PATH . 'static/css/admin.min.css'), + 'app.min.css' => \hash_file('sha384', APP_VERSION_ROOT_PATH . 'static/css/app.min.css'), + ]; + } + } + return $aSri; + } } diff --git a/snappymail/v/0.0.0/app/templates/Index.html b/snappymail/v/0.0.0/app/templates/Index.html index bd5c60d07..8970039ec 100644 --- a/snappymail/v/0.0.0/app/templates/Index.html +++ b/snappymail/v/0.0.0/app/templates/Index.html @@ -9,7 +9,7 @@ - + diff --git a/tasks/sri.js b/tasks/sri.js new file mode 100644 index 000000000..eef5e8b63 --- /dev/null +++ b/tasks/sri.js @@ -0,0 +1,39 @@ +/* Tachyon Webmail (c) Tachyon | Licensed under AGPL v3 */ +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); + +const { config } = require('./config'); + +function sriHash(filePath) { + const data = fs.readFileSync(filePath); + const hash = crypto.createHash('sha384').update(data).digest('base64'); + return 'sha384-' + hash; +} + +function sri(done) { + const jsMinDir = config.paths.staticMinJS; + const cssDir = config.paths.staticCSS; + + const files = [ + ['libs.min.js', jsMinDir + 'libs.min.js'], + ['app.min.js', jsMinDir + 'app.min.js'], + ['admin.min.js', jsMinDir + 'admin.min.js'], + ['app.min.css', cssDir + 'app.min.css'], + ['admin.min.css',cssDir + 'admin.min.css'], + ]; + + const hashes = {}; + for (const [key, filePath] of files) { + if (fs.existsSync(filePath)) { + hashes[key] = sriHash(filePath); + } + } + + const outPath = path.join('snappymail/v/' + config.devVersion, 'static/sri.json'); + fs.writeFileSync(outPath, JSON.stringify(hashes, null, '\t') + '\n'); + console.log('[sri] wrote ' + outPath); + done(); +} + +exports.sri = sri;