fix(review): open-redirect + btoa + honest duplicate-mode txn #249

Merged
jmiller merged 2 commits from fix/prerelease-review into dev 2026-07-12 22:14:44 +00:00
16 changed files with 66 additions and 28 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 02.58.35
# VERSION: 02.58.36
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 02.58.35
VERSION: 02.58.36
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -17,7 +17,7 @@
display label there.
-->
<name>MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -0,0 +1 @@
/* 02.58.36 — no schema changes */
@@ -125,11 +125,23 @@ class SnapshotRestoreEngine
$prefix = $db->getPrefix();
$totalRows = 0;
try {
$db->transactionStart();
// Build list of tables to restore based on selected types
$tablesToRestore = $this->getTablesToRestore($restoreTypes);
// Build list of tables to restore based on selected types
$tablesToRestore = $this->getTablesToRestore($restoreTypes);
/* Duplicate mode inserts brand-new records through Joomla's Table API,
whose Nested tables (categories, tags) issue LOCK TABLES — which
implicitly COMMITS any open transaction in MySQL/InnoDB. Wrapping it
in transactionStart()/Rollback() would be a false atomicity promise:
the first category/tag store would commit and defeat the rollback.
Duplicate mode is therefore best-effort and per-item defensive (a bad
item is skipped and logged, never fatal) rather than all-or-nothing.
The raw-DB modes (replace/create/merge) remain fully transactional. */
$useTransaction = ($mode !== 'duplicate');
try {
if ($useTransaction) {
$db->transactionStart();
}
if ($mode === 'duplicate') {
$totalRows = $this->restoreDuplicate($data, $restoreTypes);
@@ -156,7 +168,9 @@ class SnapshotRestoreEngine
}
}
$db->transactionCommit();
if ($useTransaction) {
$db->transactionCommit();
}
$this->log('Restore complete: ' . $totalRows . ' total rows');
@@ -188,11 +202,15 @@ class SnapshotRestoreEngine
'log' => implode("\n", $this->log),
];
} catch (\Throwable $e) {
try {
$db->transactionRollback();
$this->log('Transaction rolled back');
} catch (\Exception $rollbackEx) {
$this->log('Rollback failed: ' . $rollbackEx->getMessage());
if ($useTransaction) {
try {
$db->transactionRollback();
$this->log('Transaction rolled back');
} catch (\Exception $rollbackEx) {
$this->log('Rollback failed: ' . $rollbackEx->getMessage());
}
} else {
$this->log('Duplicate mode is not transactional; records already inserted are left in place.');
}
$this->log('FATAL: ' . $e->getMessage());
@@ -34,6 +34,14 @@ if ($this->returnUrl !== '') {
$decoded = base64_decode($this->returnUrl, true);
$raw = ($decoded !== false && $decoded !== '') ? $decoded : $this->returnUrl;
/* Reject any backslash or control char outright: legitimate Joomla admin
return URLs never contain them, and a browser normalises a leading "/\"
to "//", turning a would-be root-relative path into a protocol-relative
open redirect (e.g. "/\evil.com" → "//evil.com"). */
if (preg_match('/[\x00-\x1f\x7f\\\\]/', $raw)) {
$raw = '';
}
$isAbsoluteHttp = (bool) preg_match('#^https?://#i', $raw);
$isRootRelative = isset($raw[0]) && $raw[0] === '/' && (!isset($raw[1]) || $raw[1] !== '/');
@@ -8,7 +8,7 @@
-->
<extension type="module" client="administrator" method="upgrade">
<name>Module - MokoSuiteBackup - cPanel</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -70,6 +70,22 @@
return;
}
var ret = window.location.href;
ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1';
/* Build the redirect BEFORE we swallow the click. btoa() throws on
non-Latin1 input (e.g. a UTF-8 filter value in the query string), so
encode as UTF-8 first; if it still fails, bail WITHOUT preventing the
click so Joomla's own update/uninstall proceeds normally. */
var target;
try {
target = cfg.runbackupUrl
+ '&profile_id=' + encodeURIComponent(cfg.profileId)
+ '&returnurl=' + encodeURIComponent(btoa(unescape(encodeURIComponent(ret))));
} catch (x) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
@@ -77,12 +93,7 @@
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ task: task, cids: cids }));
} catch (x) {}
var ret = window.location.href;
ret += (ret.indexOf('?') === -1 ? '?' : '&') + 'msb_resume=1';
window.location.href = cfg.runbackupUrl
+ '&profile_id=' + encodeURIComponent(cfg.profileId)
+ '&returnurl=' + encodeURIComponent(btoa(ret));
window.location.href = target;
}, true);
/* On return from the backup screen: restore the selection and re-fire. */
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>02.58.35</version>
<version>02.58.36</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>