diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7617bcc4..8b1e0be2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## 1.7.5 (not yet released)
* CHANGED: Simpler PostgreSQL table lookup query (#1361)
+* CHANGED: SRI hashes are now configurable, no longer hardcoded in templates (#1365)
## 1.7.4 (2024-07-09)
* CHANGED: Saving markdown pastes uses `.md` extension instead of `.txt` (#1293)
diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php
index f1db2644..f91c5e89 100644
--- a/cfg/conf.sample.php
+++ b/cfg/conf.sample.php
@@ -275,3 +275,9 @@ dir = PATH "data"
; signature = ""
; (optional) the URL of the YOURLS API, called to shorten a PrivateBin URL
; apiurl = "https://yourls.example.com/yourls-api.php"
+
+;[sri]
+; Subresource integrity (SRI) hashes used in template files. Uncomment and set
+; these for all js files used. See:
+; https://github.com/PrivateBin/PrivateBin/wiki/FAQ#user-content-how-to-make-privatebin-work-when-i-have-changed-some-javascript-files
+;privatebin.js = sha512-[…]
diff --git a/lib/Configuration.php b/lib/Configuration.php
index 59722d15..3e014dc8 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -98,6 +98,23 @@ class Configuration
'signature' => '',
'apiurl' => '',
),
+ // update this array when adding/changing/removing js files
+ 'sri' => array(
+ 'js/base-x-4.0.0.js' => 'sha512-nNPg5IGCwwrveZ8cA/yMGr5HiRS5Ps2H+s0J/mKTPjCPWUgFGGw7M5nqdnPD3VsRwCVysUh3Y8OWjeSKGkEQJQ==',
+ 'js/base64-1.7.js' => 'sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==',
+ 'js/bootstrap-3.4.1.js' => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
+ 'js/bootstrap-5.3.3.js' => 'sha512-in2rcOpLTdJ7/pw5qjF4LWHFRtgoBDxXCy49H4YGOcVdGiPaQucGIbOqxt1JvmpvOpq3J/C7VTa0FlioakB2gQ==',
+ 'js/dark-mode-switch.js' => 'sha512-CCbdHdeWDbDO7aqFFmhgnvFESzaILHbUYmbhNjTpcjyO/XYdouQ9Pw8W9rpV8oJT1TsK5FbwSHU1oazmnb7BWA==',
+ 'js/jquery-3.7.1.js' => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
+ 'js/kjua-0.9.0.js' => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==',
+ 'js/legacy.js' => 'sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==',
+ 'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
+ 'js/privatebin.js' => 'sha512-cbmXvtZ/5gZPFjQDzP3IEhUAIhFPAoM31gw2kRYCT5xOh8wv9gXeDqI/t798luRW1xdC4gaYodjEFCzrsZR4mA==',
+ 'js/purify-3.1.3.js' => 'sha512-t/FKG/ucQVMWTWVouSMABSEx1r+uSyAI9eNDq0KEr9mPhkgxpJztHI/E72JIpv/+VwPs/Q4husxj14TE9Ps/wg==',
+ 'js/rawinflate-0.3.js' => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==',
+ 'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
+ 'js/zlib-1.3.1.js' => 'sha512-Z90oppVx/mn0DG2k9airjFVQuliELlXLeT3SRiO6MLiUSbhGlAq+UFwmYbG4i9mwW87dkG8fgJPapGwnUq7Osg==',
+ ),
);
/**
@@ -193,6 +210,10 @@ class Configuration
}
// check for missing keys and set defaults if necessary
else {
+ // preserve configured SRI hashes
+ if ($section == 'sri' && array_key_exists($section, $config)) {
+ $this->_configuration[$section] = $config[$section];
+ }
foreach ($values as $key => $val) {
if ($key == 'dir') {
$val = PATH . $val;
diff --git a/lib/Controller.php b/lib/Controller.php
index c1c4f2ba..58d1369d 100644
--- a/lib/Controller.php
+++ b/lib/Controller.php
@@ -436,6 +436,7 @@ class Controller
$page->assign('HTTPWARNING', $this->_conf->getKey('httpwarning'));
$page->assign('HTTPSLINK', 'https://' . $this->_request->getHost() . $this->_request->getRequestUri());
$page->assign('COMPRESSION', $this->_conf->getKey('compression'));
+ $page->assign('SRI', $this->_conf->getSection('sri'));
$page->draw($this->_conf->getKey('template'));
}
diff --git a/lib/View.php b/lib/View.php
index 395ac86d..958be833 100644
--- a/lib/View.php
+++ b/lib/View.php
@@ -57,4 +57,23 @@ class View
extract($this->_variables);
include $path;
}
+
+ /**
+ * echo script tag incl. SRI hash for given script file
+ *
+ * @access private
+ * @param string $file
+ * @param string $attributes additional attributes to add into the script tag
+ */
+ private function _scriptTag($file, $attributes = '')
+ {
+ $sri = array_key_exists($file, $this->_variables['SRI']) ?
+ ' integrity="' . $this->_variables['SRI'][$file] . '"' : '';
+ // if the file isn't versioned (ends in a digit), add our own version
+ $cacheBuster = ctype_digit(substr($file, -4, 1)) ?
+ '' : '?' . rawurlencode($this->_variables['VERSION']);
+ echo '', PHP_EOL;
+ }
}
diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php
index e9a15ef1..9f266615 100644
--- a/tpl/bootstrap.php
+++ b/tpl/bootstrap.php
@@ -42,38 +42,38 @@ if ($SYNTAXHIGHLIGHTING) :
endif;
?>
-
+ _scriptTag('js/jquery-3.7.1.js', 'async'); ?>
-
+ _scriptTag('js/kjua-0.9.0.js', 'async'); ?>
-
+ _scriptTag('js/base64-1.7.js', 'async'); ?>
-
-
-
-
+ _scriptTag('js/zlib-1.3.1.js', 'async'); ?>
+ _scriptTag('js/base-x-4.0.0.js', 'async'); ?>
+ _scriptTag('js/rawinflate-0.3.js', 'async'); ?>
+ _scriptTag('js/bootstrap-3.4.1.js', 'defer'); ?>
-
+ _scriptTag('js/prettify.js', 'async'); ?>
-
+ _scriptTag('js/showdown-2.1.0.js', 'async'); ?>
-
-
-
+ _scriptTag('js/purify-3.1.3.js', 'async'); ?>
+ _scriptTag('js/legacy.js', 'async'); ?>
+ _scriptTag('js/privatebin.js', 'defer'); ?>
diff --git a/tpl/bootstrap5.php b/tpl/bootstrap5.php
index 9bf7feb0..64bb9677 100644
--- a/tpl/bootstrap5.php
+++ b/tpl/bootstrap5.php
@@ -25,39 +25,39 @@ if ($SYNTAXHIGHLIGHTING) :
endif;
?>
-
+ _scriptTag('js/jquery-3.7.1.js', 'defer'); ?>
-
+ _scriptTag('js/kjua-0.9.0.js', 'async'); ?>
-
+ _scriptTag('js/base64-1.7.js', 'async'); ?>
-
-
-
-
-
+ _scriptTag('js/zlib-1.3.1.js', 'async'); ?>
+ _scriptTag('js/base-x-4.0.0.js', 'async'); ?>
+ _scriptTag('js/rawinflate-0.3.js', 'async'); ?>
+ _scriptTag('js/bootstrap-5.3.3.js', 'async'); ?>
+ _scriptTag('js/dark-mode-switch.js', 'async'); ?>
-
+ _scriptTag('js/prettify.js', 'async'); ?>
-
+ _scriptTag('js/showdown-2.1.0.js', 'async'); ?>
-
-
-
+ _scriptTag('js/purify-3.1.3.js', 'async'); ?>
+ _scriptTag('js/legacy.js', 'async'); ?>
+ _scriptTag('js/privatebin.js', 'defer'); ?>
diff --git a/tpl/page.php b/tpl/page.php
index 47c55203..79bf482f 100644
--- a/tpl/page.php
+++ b/tpl/page.php
@@ -21,37 +21,37 @@ if ($SYNTAXHIGHLIGHTING):
endif;
endif;
?>
-
+ _scriptTag('js/jquery-3.7.1.js', 'defer'); ?>
-
+ _scriptTag('js/kjua-0.9.0.js', 'async'); ?>
-
+ _scriptTag('js/base64-1.7.js', 'async'); ?>
-
-
-
+ _scriptTag('js/zlib-1.3.1.js', 'async'); ?>
+ _scriptTag('js/base-x-4.0.0.js', 'async'); ?>
+ _scriptTag('js/rawinflate-0.3.js', 'async'); ?>
-
+ _scriptTag('js/prettify.js', 'async'); ?>
-
+ _scriptTag('js/showdown-2.1.0.js', 'async'); ?>
-
-
-
+ _scriptTag('js/purify-3.1.3.js', 'async'); ?>
+ _scriptTag('js/legacy.js', 'async'); ?>
+ _scriptTag('js/privatebin.js', 'defer'); ?>
diff --git a/tst/Bootstrap.php b/tst/Bootstrap.php
index 984f7d50..28b56422 100644
--- a/tst/Bootstrap.php
+++ b/tst/Bootstrap.php
@@ -371,42 +371,33 @@ class Helper
*/
public static function updateSubresourceIntegrity()
{
- $dir = dir(PATH . 'js');
- while (false !== ($file = $dir->read())) {
- if (substr($file, -3) === '.js') {
- self::$hashes[$file] = base64_encode(
- hash('sha512', file_get_contents(
- PATH . 'js' . DIRECTORY_SEPARATOR . $file
- ), true)
- );
+ foreach (new GlobIterator(PATH . 'js' . DIRECTORY_SEPARATOR . '*.js') as $file) {
+ if ($file->getBasename() == 'common.js') {
+ continue; // ignore JS unit test bootstrap
}
+ self::$hashes[$file->getBasename()] = base64_encode(
+ hash('sha512', file_get_contents($file->getPathname()), true)
+ );
}
- $dir = dir(PATH . 'tpl');
- while (false !== ($file = $dir->read())) {
- if (substr($file, -4) === '.php') {
- $content = file_get_contents(
- PATH . 'tpl' . DIRECTORY_SEPARATOR . $file
- );
- $content = preg_replace_callback(
- '##',
- function ($matches) {
- if (array_key_exists($matches[2], Helper::$hashes)) {
- return '';
- } else {
- return $matches[0];
- }
- },
- $content
- );
- file_put_contents(
- PATH . 'tpl' . DIRECTORY_SEPARATOR . $file,
- $content
- );
- }
+ $counter = 0;
+ $file = PATH . 'lib' . DIRECTORY_SEPARATOR . 'Configuration.php';
+ $content = preg_replace_callback(
+ '#\'js/([a-z0-9.-]+.js)(\' +)=\> \'[^\']*\',#',
+ function ($matches) use (&$counter) {
+ if (array_key_exists($matches[1], Helper::$hashes)) {
+ ++$counter;
+ return '\'js/' . $matches[1] . $matches[2] .
+ '=> \'sha512-' . Helper::$hashes[$matches[1]] . '\',';
+ } else {
+ throw new Exception('SRI hash for file js/' . $matches[1] . ' not found, please add the missing file or remove it from lib/Configuration.php.');
+ }
+ },
+ file_get_contents($file)
+ );
+ file_put_contents($file, $content);
+ if ($counter != count(self::$hashes)) {
+ throw new Exception('Mismatch between ' . count(self::$hashes) . ' found js files and ' . $counter . ' SRI hashes in lib/Configuration.php, please update lib/Configuration.php to match the list of js files.');
}
}
}
diff --git a/tst/ConfigurationTest.php b/tst/ConfigurationTest.php
index 27a31f73..d3e87387 100644
--- a/tst/ConfigurationTest.php
+++ b/tst/ConfigurationTest.php
@@ -120,9 +120,15 @@ class ConfigurationTest extends TestCase
unset($options['expire_options']['1week']);
unset($options['expire_options']['1year']);
unset($options['expire_options']['never']);
+ $sri_key = array_key_first($options['sri']);
+ $valid_sri = $options['sri'][$sri_key];
+ $options['sri'][$sri_key] = ''; // empty string should get replaced with default
+ $options['sri']['js/example.js'] = 'some invalid SRI hash';
Helper::createIniFile(CONF, $options);
$conf = new Configuration;
+ // restore expected results
$options['expire']['default'] = '5min';
+ $options['sri'][$sri_key] = $valid_sri;
$this->assertEquals($options, $conf->get(), 'not overriding "missing" subkeys');
}
diff --git a/tst/ViewTest.php b/tst/ViewTest.php
index 2a1c7e47..65b252de 100644
--- a/tst/ViewTest.php
+++ b/tst/ViewTest.php
@@ -63,6 +63,7 @@ class ViewTest extends TestCase
$page->assign('HTTPSLINK', 'https://example.com/');
$page->assign('COMPRESSION', 'zlib');
$page->assign('CSPHEADER', 'default-src \'none\'');
+ $page->assign('SRI', array());
$dir = dir(PATH . 'tpl');
while (false !== ($file = $dir->read())) {