From 124a9c687eac2060838b8d24c73f9e985e177352 Mon Sep 17 00:00:00 2001 From: the-djmaze <3752035+the-djmaze@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:10:41 +0100 Subject: [PATCH] Added Subresource Integrity checks from Tachyon https://github.com/the-djmaze/snappymail/commit/64e23d4fca6c458d9baa5282216a88ddd24cfad5 --- dev/boot.js | 8 ++-- gulpfile.js | 4 +- .../0.0.0/app/libraries/RainLoop/Actions.php | 6 +++ .../0.0.0/app/libraries/RainLoop/Service.php | 5 ++- .../app/libraries/snappymail/integrity.php | 26 +++++++++++-- snappymail/v/0.0.0/app/templates/Index.html | 2 +- tasks/sri.js | 39 +++++++++++++++++++ 7 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 tasks/sri.js 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 @@