Add security verification for untrusted archives #587 (#588)

This commit is contained in:
Jaifroid 2024-04-19 15:26:29 +01:00 committed by GitHub
parent 0c833bae77
commit 7c40edd189
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 51 deletions

View File

@ -2,7 +2,9 @@
## In-progress release 3.2.2 ## In-progress release 3.2.2
* FEATURE: Add security dialogue on opening a ZIM for the first time in ServiceWorker mode
* UPDATE: Rename JQuery mode to Restricted mode * UPDATE: Rename JQuery mode to Restricted mode
* FIX: Cached last page sometimes overwrites new ZIM landing page when switching from Restricted mode
* FIX: Display of open/close marker with h5 and h6 headings in Wikimedia ZIMs * FIX: Display of open/close marker with h5 and h6 headings in Wikimedia ZIMs
* FIX: Inability to print HTML books in Gutenberg ZIMs * FIX: Inability to print HTML books in Gutenberg ZIMs
* FIX: Bug in JQuery mode which made all images load as manual display areas in some non-Wikimedia ZIMs * FIX: Bug in JQuery mode which made all images load as manual display areas in some non-Wikimedia ZIMs

View File

@ -1252,6 +1252,11 @@
<span class="checkmark"></span> <span class="checkmark"></span>
<b>Disable drag-and-drop</b> (in case it is causing anomalies) <b>Disable drag-and-drop</b> (in case it is causing anomalies)
</label> </label>
<label class="checkbox" data-i18n-tip="configure-expert-enable-source-verification-tip" title="Warning: Some ZIM archives from untrusted sources could run malicious code in your browser. This can be prevented by using Restricted mode, which cannot run active content from the ZIM. Highly dynamic ZIMs will probably fail in Restricted mode, but ZIMs with largely static content should work. If you trust the source of all of your ZIMs, then disabling this option will use ServiceWorker mode by default, if available.">
<input type="checkbox" name="disableFileVerification" id="enableSourceVerificationCheck" >
<span data-i18n="configure-expert-enable-source-verification-check-box" class="checkmark"></span>
<b>Enable source verification of new files</b> (<i>recommended</i>: you will only be prompted the first time you open a ZIM)
</label>
<div id="expressPortInputDiv" style="display: none;" title="If you allowed network access on startup, you can access this app from any local browser by going to localhost:port in the browser address bar (e.g. http://localhost:3000)."> <div id="expressPortInputDiv" style="display: none;" title="If you allowed network access on startup, you can access this app from any local browser by going to localhost:port in the browser address bar (e.g. http://localhost:3000).">
<b>Customize the localhost port when accessing this app from a browser</b>:<br /> <b>Customize the localhost port when accessing this app from a browser</b>:<br />
<label> <label>

View File

@ -464,7 +464,7 @@ function printIntercept () {
return uiUtil.systemAlert('Sorry, we could not find a document to print! Please load one first.', 'Warning'); return uiUtil.systemAlert('Sorry, we could not find a document to print! Please load one first.', 'Warning');
} }
if (params.contentInjectionMode === 'serviceworker') { if (params.contentInjectionMode === 'serviceworker') {
// Re-establishe lastPageVisit because it is not always set, for example with dynamic loads, in SW mode // Re-establish lastPageVisit because it is not always set, for example with dynamic loads, in SW mode
params.lastPageVisit = articleDocument.location.href.replace(/^.+\/([^/]+\.[zZ][iI][mM]\w?\w?)\/([CA]\/.*$)/, function (m0, zimName, zimURL) { params.lastPageVisit = articleDocument.location.href.replace(/^.+\/([^/]+\.[zZ][iI][mM]\w?\w?)\/([CA]\/.*$)/, function (m0, zimName, zimURL) {
return decodeURI(zimURL) + '@kiwixKey@' + decodeURI(zimName); return decodeURI(zimURL) + '@kiwixKey@' + decodeURI(zimName);
}); });
@ -1689,15 +1689,27 @@ document.querySelectorAll('input[name="contentInjectionMode"][type="radio"]').fo
} }
// Do the necessary to enable or disable the Service Worker // Do the necessary to enable or disable the Service Worker
setContentInjectionMode(this.value); setContentInjectionMode(this.value);
// If we're in a PWA UWP app, warn the user that this does not disable the PWA
if (this.value === 'jquery' && /^http/i.test(window.location.protocol) && /UWP\|PWA/.test(params.appType) && /** DEV: PLEASE NOTE THAT "jQuery mode" HAS NOW CHANGED to "Restricted mode", but we still use "jquery" in code */
params.allowInternetAccess === 'true') {
uiUtil.systemAlert( // Actions that must be completed after switch to Restricted mode
'<p>Please note that switching content injection mode does not revert to local code.</p>' + if (this.value === 'jquery') {
'<p>If you wish to exit the PWA, you will need to turn off "Allow Internet access?" above.</p>' // Hide the source verification option
); document.getElementById('enableSourceVerificationCheck').style.display = 'none';
// If we're in a PWA UWP app, warn the user that this does not disable the PWA
if (/^http/i.test(window.location.protocol) && /UWP\|PWA/.test(params.appType) &&
params.allowInternetAccess === 'true') {
uiUtil.systemAlert(
'<p>Please note that switching content injection mode does not revert to local code.</p>' +
'<p>If you wish to exit the PWA, you will need to turn off "Allow Internet access?" above.</p>'
);
}
} }
if (this.value === 'serviceworker') { if (this.value === 'serviceworker') {
document.getElementById('enableSourceVerificationCheck').style.display = '';
if (appstate.selectedArchive.isReady() && !(settingsStore.getItem('trustedZimFiles').includes(appstate.selectedArchive.file.name)) && params.sourceVerification) {
verifyLoadedArchive(appstate.selectedArchive);
}
if (params.manipulateImages || params.allowHTMLExtraction) { if (params.manipulateImages || params.allowHTMLExtraction) {
if (!appstate.wikimediaZimLoaded) { if (!appstate.wikimediaZimLoaded) {
var message = 'Please note that we are disabling "Image manipulation" and/or "Download or open current article" features, as these options ' + var message = 'Please note that we are disabling "Image manipulation" and/or "Download or open current article" features, as these options ' +
@ -1855,6 +1867,12 @@ document.getElementById('disableDragAndDropCheck').addEventListener('change', fu
} }
}); });
}); });
// Source verification is only makes sense in SW mode as doing the same in jQuery mode is redundant.
document.getElementById('enableSourceVerificationCheck').style.display = params.contentInjectionMode === ('serviceworker' || 'serviceworkerlocal') ? 'block' : 'none';
document.getElementById('enableSourceVerificationCheck').addEventListener('change', function () {
params.sourceVerification = this.checked;
settingsStore.setItem('sourceVerification', this.checked, Infinity);
});
document.getElementById('hideActiveContentWarningCheck').addEventListener('change', function () { document.getElementById('hideActiveContentWarningCheck').addEventListener('change', function () {
params.hideActiveContentWarning = this.checked; params.hideActiveContentWarning = this.checked;
settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity); settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
@ -2418,12 +2436,12 @@ function switchCSSTheme () {
}, 100); }, 100);
// If the interval has not succeeded after 3 seconds, give up // If the interval has not succeeded after 3 seconds, give up
if (zimitIframe && document.getElementById('configuration').style.display === 'none') { if (zimitIframe && document.getElementById('configuration').style.display === 'none') {
setTimeout(function () { setTimeout(function (zimitf, articleC) {
articleContainer.style.display = ''; articleC.style.display = '';
zimitIframe.style.display = ''; zimitf.style.display = '';
clearInterval(interval); clearInterval(interval);
window.dispatchEvent(new Event('resize')); // Force repaint window.dispatchEvent(new Event('resize')); // Force repaint
}, 3000); }, 3000, zimitIframe, articleContainer);
} }
} else if (document.getElementById('configuration').style.display === 'none') { } else if (document.getElementById('configuration').style.display === 'none') {
// We're dealing with a light style, so we just display it // We're dealing with a light style, so we just display it
@ -4067,6 +4085,38 @@ function setLocalArchiveFromFileList (files, fromArchiveList) {
}); });
} }
/**
* Verifies the given archive and switches contentInjectionMode accourdingly
* Code to undertake the verification adapted from kiwix/kiwix-js #1192 kindly authored by @Greeshmanth1909
*
* @param {Object} archive The archive that needs verification
*
*/
function verifyLoadedArchive (archive) {
return uiUtil.systemAlert('<p>Is this ZIM archive from a trusted source?</p><p style="border: 1px solid;padding:5px;">' +
'Name:&nbsp;<b>' + archive.file.name + '</b><br />' +
'Creator:&nbsp;<b>' + archive.creator + '</b><br />' +
'Publisher:&nbsp;<b>' + archive.publisher + '</b><br />' +
'Scraper:&nbsp;<b>' + archive.scraper + '</b><br />' +
'</p><p><b><i>Warning: above data can easily be spoofed!</i></b></p>' +
'</p><p>If you do not trust the source, you can still read the ZIM file in Restricted mode. Closing this window also opens the file in Restricted mode.</p>' +
'<p><i>If you mark the file as trusted, this alert will not show again.</i> (Security checks can be disabled in Expert Settings.)</p>',
'Security alert!', true, 'Open in Restricted mode', 'Trust source').then(response => {
if (response) {
params.contentInjectionMode = 'serviceworker';
var trustedZimFiles = settingsStore.getItem('trustedZimFiles');
var updatedTrustedZimFiles = trustedZimFiles + archive.file.name + '|';
settingsStore.setItem('trustedZimFiles', updatedTrustedZimFiles, Infinity);
// Change radio buttons accordingly
document.getElementById('serviceworkerModeRadio').checked = true;
} else {
// Switch to Restricted mode
params.contentInjectionMode = 'jquery';
document.getElementById('jQueryModeRadio').checked = true;
}
});
}
/** /**
* Functions to be run immediately after the archive is loaded * Functions to be run immediately after the archive is loaded
* *
@ -4185,27 +4235,57 @@ function archiveReadyCallback (archive) {
} }
// This ensures the correct icon is set for the newly loaded archive // This ensures the correct icon is set for the newly loaded archive
cssUIThemeGetOrSet(params.cssUITheme); cssUIThemeGetOrSet(params.cssUITheme);
if (params.rescan) { var displayArchive = function () {
document.getElementById('btnConfigure').click(); if (params.rescan) {
setTimeout(function () {
document.getElementById('btnConfigure').click(); document.getElementById('btnConfigure').click();
params.rescan = false; setTimeout(function () {
}, 100); document.getElementById('btnConfigure').click();
} else { params.rescan = false;
if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !params.useOPFS && !window.dialog) { }, 100);
document.getElementById('instructions').style.display = 'none';
} else { } else {
document.getElementById('openLocalFiles').style.display = 'none'; if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !params.useOPFS && !window.dialog) {
document.getElementById('rescanStorage').style.display = 'block'; document.getElementById('instructions').style.display = 'none';
} } else {
document.getElementById('usage').style.display = 'none'; document.getElementById('openLocalFiles').style.display = 'none';
if (params.rememberLastPage && ~params.lastPageVisit.indexOf(params.storedFile.replace(/\.zim(\w\w)?$/, ''))) { document.getElementById('rescanStorage').style.display = 'block';
var lastPage = params.lastPageVisit.replace(/@kiwixKey@.+/, ''); }
goToArticle(lastPage); document.getElementById('usage').style.display = 'none';
} else { if (params.rememberLastPage && ~params.lastPageVisit.indexOf(params.storedFile.replace(/\.zim(\w\w)?$/, ''))) {
document.getElementById('btnHome').click(); var lastPage = params.lastPageVisit.replace(/@kiwixKey@.+/, '');
goToArticle(lastPage);
} else {
document.getElementById('btnHome').click();
}
} }
} }
// Set contentInjectionMode to serviceWorker when opening a new archive in case the user switched to Restricted mode/jQuery Mode when opening the previous archive
if (params.contentInjectionMode === 'jquery') {
params.contentInjectionMode = settingsStore.getItem('contentInjectionMode');
// Change the radio buttons accordingly
switch (settingsStore.getItem('contentInjectionMode')) {
case 'serviceworker':
document.getElementById('serviceworkerModeRadio').checked = true;
// In case we atuo-switched off assetsCache due to switch to Restricted mode, we need to reset
params.assetsCache = settingsStore.getItem('asetsCache') !== 'false';
break;
case 'serviceworkerlocal':
document.getElementById('serviceworkerLocalModeRadio').checked = true;
break;
}
}
if (settingsStore.getItem('trustedZimFiles') === null) {
settingsStore.setItem('trustedZimFiles', '', Infinity);
}
if (params.sourceVerification && (params.contentInjectionMode === 'serviceworker' || params.contentInjectionMode === 'serviceworkerlocal')) {
// Check if source of the zim file can be trusted.
if (!(settingsStore.getItem('trustedZimFiles').includes(archive.file.name))) {
verifyLoadedArchive(archive).then(function () {
displayArchive();
});
return;
}
}
displayArchive();
} }
function loadPackagedArchive () { function loadPackagedArchive () {
@ -5119,6 +5199,21 @@ var articleLoadedSW = function (dirEntry, container) {
uiUtil.showSlidingUIElements(); uiUtil.showSlidingUIElements();
var doc = articleWindow ? articleWindow.document : null; var doc = articleWindow ? articleWindow.document : null;
articleDocument = doc; articleDocument = doc;
var mimeType = dirEntry.getMimetype();
// If we've successfully loaded an HTML document...
if (doc && /\bx?html/i.test(mimeType)) {
if (params.rememberLastPage) {
params.lastPageVisit = dirEntry.namespace + '/' + dirEntry.url + '@kiwixKey@' + appstate.selectedArchive.file.name;
} else {
params.lastPageVisit = '';
}
// Turn off failsafe for SW mode
settingsStore.setItem('lastPageLoad', 'OK', Infinity);
settingsStore.setItem('lastPageVisit', params.lastPageVisit, Infinity);
// Set or clear the ZIM store of last page
var lastPage = params.rememberLastPage ? dirEntry.namespace + '/' + dirEntry.url : '';
settingsStore.setItem(appstate.selectedArchive.file.name, lastPage, Infinity);
}
var docBody = doc ? doc.body : null; var docBody = doc ? doc.body : null;
if (docBody) { if (docBody) {
// Trap clicks in the iframe to enable us to work around the sandbox when opening external links and PDFs // Trap clicks in the iframe to enable us to work around the sandbox when opening external links and PDFs
@ -5133,7 +5228,6 @@ var articleLoadedSW = function (dirEntry, container) {
listenForSearchKeys(); listenForSearchKeys();
} }
// Note that switchCSSTheme() requires access to params.lastPageVisit // Note that switchCSSTheme() requires access to params.lastPageVisit
params.lastPageVisit = dirEntry.namespace + '/' + dirEntry.url + '@kiwixKey@' + appstate.selectedArchive.file.name;
if (!appstate.isReplayWorkerAvailable) switchCSSTheme(); // Gets called in articleLoader for replay_iframe if (!appstate.isReplayWorkerAvailable) switchCSSTheme(); // Gets called in articleLoader for replay_iframe
if (appstate.selectedArchive.zimType === 'open') { if (appstate.selectedArchive.zimType === 'open') {
// Set relative font size + Stackexchange-family multiplier // Set relative font size + Stackexchange-family multiplier
@ -5183,12 +5277,6 @@ var articleLoadedSW = function (dirEntry, container) {
resizeIFrame(); resizeIFrame();
}, 200); }, 200);
} }
// Turn off failsafe for SW mode
settingsStore.setItem('lastPageLoad', 'OK', Infinity);
if (!appstate.isReplayWorkerAvailable) {
// Because this is loading within docBody, it should only get set for HTML documents
if (params.rememberLastPage) settingsStore.setItem('lastPageVisit', params.lastPageVisit, Infinity);
}
uiUtil.clearSpinner(); uiUtil.clearSpinner();
// If we reloaded the page to print the desktop style, we need to return to the printIntercept dialogue // If we reloaded the page to print the desktop style, we need to return to the printIntercept dialogue
if (params.printIntercept) printIntercept(); if (params.printIntercept) printIntercept();
@ -5205,7 +5293,10 @@ var articleLoadedSW = function (dirEntry, container) {
if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl()); if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl());
params.isLandingPage = false; params.isLandingPage = false;
} else { } else {
loaded = false; // If we havent' loaded a text-type document, we probably haven't finished loading
if (!/^text\//i.test(mimeType)) {
loaded = false;
}
} }
// Show spinner when the article unloads // Show spinner when the article unloads
@ -6253,7 +6344,8 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
if (downloadAlert) downloadAlert.style.display = 'none'; if (downloadAlert) downloadAlert.style.display = 'none';
// Code below will run after we have written the new article to the articleContainer // Code below will run after we have written the new article to the articleContainer
var articleLoaded = params.contentInjectionMode === 'serviceworker' ? function () {} : function () { var articleLoaded = function () {
if (params.contentInjectionMode === 'serviceworker') return;
// Set a global error handler for articleWindow // Set a global error handler for articleWindow
articleWindow.onerror = function (msg, url, line, col, error) { articleWindow.onerror = function (msg, url, line, col, error) {
console.error('Error caught in ZIM contents [' + url + ':' + line + ']:\n' + msg, error); console.error('Error caught in ZIM contents [' + url + ':' + line + ']:\n' + msg, error);
@ -6379,17 +6471,9 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
// Make sure the article area is displayed // Make sure the article area is displayed
setTab(); setTab();
checkToolbar(); checkToolbar();
var showArticle = function () { // Show the article
articleDocument.bgcolor = ''; articleDocument.bgcolor = '';
docBody.style.display = 'block'; docBody.style.display = 'block';
};
if ('MSBlobBuilder' in window) {
// For legacy MS browsers, including UWP, delay causes blank screen on slow systems
showArticle();
} else {
// For Chromium browsers a small delay greatly improves composition
setTimeout(showArticle, 80);
}
// Jump to any anchor parameter // Jump to any anchor parameter
if (anchorParameter) { if (anchorParameter) {
var target = articleWindow.document.getElementById(anchorParameter); var target = articleWindow.document.getElementById(anchorParameter);

View File

@ -110,6 +110,7 @@ params['rightClickType'] = getSetting('rightClickType'); // 'single|double|false
params['navButtonsPos'] = getSetting('navButtonsPos') || 'bottom'; // 'top|bottom' A setting that determines where the back-forward nav buttons appear params['navButtonsPos'] = getSetting('navButtonsPos') || 'bottom'; // 'top|bottom' A setting that determines where the back-forward nav buttons appear
params['useOPFS'] = getSetting('useOPFS') === true; // A setting that determines whether to use OPFS (experimental) params['useOPFS'] = getSetting('useOPFS') === true; // A setting that determines whether to use OPFS (experimental)
params['useLegacyZimitSupport'] = getSetting('useLegacyZimitSupport') === true; // A setting that determines whether to force the use of legacy Zimit support params['useLegacyZimitSupport'] = getSetting('useLegacyZimitSupport') === true; // A setting that determines whether to force the use of legacy Zimit support
params['sourceVerification'] = params.contentInjectionMode === 'serviceworker' ? (getSetting('sourceVerification') === null ? true : getSetting('sourceVerification')) : false; // Sets a boolean indicating weather a user trusts the source of zim files
// Do not touch these values unless you know what they do! Some are global variables, some are set programmatically // Do not touch these values unless you know what they do! Some are global variables, some are set programmatically
params['cacheAPI'] = 'kiwixjs-assetsCache'; // Set the global Cache API database or cache name here, and synchronize with Service Worker params['cacheAPI'] = 'kiwixjs-assetsCache'; // Set the global Cache API database or cache name here, and synchronize with Service Worker
@ -251,6 +252,7 @@ document.getElementById('rememberLastPageCheck').checked = params.rememberLastPa
document.getElementById('displayFileSelectorsCheck').checked = params.showFileSelectors; document.getElementById('displayFileSelectorsCheck').checked = params.showFileSelectors;
document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning; document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning;
document.getElementById('useLibzimReaderCheck').checked = params.useLibzim; document.getElementById('useLibzimReaderCheck').checked = params.useLibzim;
document.getElementById('enableSourceVerificationCheck').checked = getSetting('sourceVerification') === null ? true : getSetting('sourceVerification');
document.getElementById('useLegacyZimitSupportCheck').checked = params.useLegacyZimitSupport; document.getElementById('useLegacyZimitSupportCheck').checked = params.useLegacyZimitSupport;
document.getElementById('alphaCharTxt').value = params.alphaChar; document.getElementById('alphaCharTxt').value = params.alphaChar;
document.getElementById('omegaCharTxt').value = params.omegaChar; document.getElementById('omegaCharTxt').value = params.omegaChar;

View File

@ -80,7 +80,8 @@ function ZIMArchive (storage, path, callbackReady, callbackError) {
// Further metadata are added in the background below, and can be accessed later // Further metadata are added in the background below, and can be accessed later
return Promise.all([ return Promise.all([
that.addMetadataToZIMFile('Creator'), that.addMetadataToZIMFile('Creator'),
that.addMetadataToZIMFile('Language') that.addMetadataToZIMFile('Language'),
that.addMetadataToZIMFile('Publisher')
]).then(function () { ]).then(function () {
console.debug('ZIMArchive ready, metadata will be added in the background'); console.debug('ZIMArchive ready, metadata will be added in the background');
// Add non-time-critical metadata to archive in background so as not to delay opening of the archive // Add non-time-critical metadata to archive in background so as not to delay opening of the archive
@ -92,7 +93,6 @@ function ZIMArchive (storage, path, callbackReady, callbackError) {
that.addMetadataToZIMFile('Date'), that.addMetadataToZIMFile('Date'),
that.addMetadataToZIMFile('Description'), that.addMetadataToZIMFile('Description'),
that.addMetadataToZIMFile('Name'), that.addMetadataToZIMFile('Name'),
that.addMetadataToZIMFile('Publisher'),
that.addMetadataToZIMFile('Source'), that.addMetadataToZIMFile('Source'),
that.addMetadataToZIMFile('Title') that.addMetadataToZIMFile('Title')
]).then(function () { ]).then(function () {