feat: SFTP key file upload, auth type dropdown, security hardening
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Build & Release / Promote to RC (pull_request) Failing after 11s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Build & Release / Promote to RC (pull_request) Failing after 11s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
SFTP UX improvements: - SshKeyField: file upload button (FileReader → base64 → hidden field), key never displayed as readable text, __KEEP_EXISTING__ sentinel preserves DB value on re-save without re-uploading - Auth type dropdown: password / key file / key file + passphrase with conditional field visibility via showon - Required field markers on host, username, path, password - Remove insecure FTP option from remote storage dropdown Security: - Private key stored base64-encoded in database - SftpUploader decodes base64 before writing temp file - ProfileTable::store() handles sentinel to prevent key leakage - Key content never rendered in HTML form output
This commit is contained in:
@@ -159,7 +159,6 @@
|
||||
default="none"
|
||||
>
|
||||
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
|
||||
<option value="ftp">COM_MOKOJOOMBACKUP_REMOTE_FTP</option>
|
||||
<option value="sftp">COM_MOKOJOOMBACKUP_REMOTE_SFTP</option>
|
||||
<option value="google_drive">COM_MOKOJOOMBACKUP_REMOTE_GDRIVE</option>
|
||||
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
|
||||
@@ -183,6 +182,7 @@
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST_DESC"
|
||||
maxlength="255"
|
||||
required="true"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
<field
|
||||
@@ -201,25 +201,38 @@
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC"
|
||||
maxlength="255"
|
||||
required="true"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
<field
|
||||
name="sftp_auth_type"
|
||||
type="list"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC"
|
||||
default="key"
|
||||
showon="remote_storage:sftp"
|
||||
>
|
||||
<option value="password">COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD</option>
|
||||
<option value="key">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY</option>
|
||||
<option value="key_passphrase">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE</option>
|
||||
</field>
|
||||
<field
|
||||
name="sftp_password"
|
||||
type="password"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp"
|
||||
required="true"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:password"
|
||||
/>
|
||||
<field
|
||||
name="sftp_key_data"
|
||||
type="textarea"
|
||||
type="SshKey"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC"
|
||||
rows="6"
|
||||
cols="60"
|
||||
filter="raw"
|
||||
showon="remote_storage:sftp"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:key,key_passphrase"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="sftp_passphrase"
|
||||
@@ -227,7 +240,8 @@
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp"
|
||||
required="true"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:key_passphrase"
|
||||
/>
|
||||
<field
|
||||
name="sftp_path"
|
||||
@@ -236,6 +250,7 @@
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH_DESC"
|
||||
default="/backups"
|
||||
maxlength="512"
|
||||
required="true"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -256,7 +256,17 @@ COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication. Leave blank if using a key file."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY="SSH Private Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Paste the contents of your SSH private key (e.g. id_rsa or id_ed25519). The key is stored securely in the database and written to a temp file with 0600 permissions only during upload, then deleted. Leave blank to use password authentication."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload or paste your SSH private key (e.g. id_rsa or id_ed25519). The key is stored securely in the database and written to a temp file with 0600 permissions only during upload, then deleted. Leave blank to use password authentication."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD="Upload Key File"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_NONE="No key file"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR="Remove Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE="Authentication Type"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC="Choose how to authenticate with the SFTP server."
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD="Password"
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY="Key File"
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE="Key File + Passphrase"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE="Key Passphrase"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC="Passphrase for the private key, if encrypted. Leave blank for unencrypted keys."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH="Remote Path"
|
||||
|
||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`sftp_host` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22,
|
||||
`sftp_username` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key',
|
||||
`sftp_password` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_key_data` MEDIUMTEXT,
|
||||
`sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- MokoSuiteBackup 01.36.00 — SFTP auth type column
|
||||
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
ADD COLUMN `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key' AFTER `sftp_username`;
|
||||
@@ -141,7 +141,15 @@ class SftpUploader implements RemoteUploaderInterface
|
||||
$tmpDir = sys_get_temp_dir();
|
||||
$keyFile = $tmpDir . '/mokobackup-sftp-' . bin2hex(random_bytes(8)) . '.key';
|
||||
|
||||
if (file_put_contents($keyFile, $this->keyData) === false) {
|
||||
/* Key is stored base64-encoded in the database — decode before writing */
|
||||
$keyContent = base64_decode($this->keyData, true);
|
||||
|
||||
if ($keyContent === false) {
|
||||
/* Fallback: might be raw PEM (legacy or paste) */
|
||||
$keyContent = $this->keyData;
|
||||
}
|
||||
|
||||
if (file_put_contents($keyFile, $keyContent) === false) {
|
||||
throw new \RuntimeException('Cannot write temporary SSH key file');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* Custom field for SSH private key input.
|
||||
* Supports both file upload (via FileReader JS) and paste-in textarea.
|
||||
* The key content is stored in the database, not as a file on disk.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Form\FormField;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
class SshKeyField extends FormField
|
||||
{
|
||||
protected $type = 'SshKey';
|
||||
|
||||
protected function getInput(): string
|
||||
{
|
||||
$value = $this->value ?? '';
|
||||
$id = $this->id;
|
||||
$name = $this->name;
|
||||
|
||||
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
||||
|
||||
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
||||
|
||||
/* Status badge */
|
||||
if ($hasKey) {
|
||||
$html .= '<span class="badge bg-success me-2">'
|
||||
. '<span class="icon-lock" aria-hidden="true"></span> '
|
||||
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED')
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
/* File upload button */
|
||||
$html .= '<label class="btn btn-outline-secondary btn-sm" for="' . htmlspecialchars($id) . '-file">';
|
||||
$html .= '<span class="icon-upload" aria-hidden="true"></span> ';
|
||||
$html .= $hasKey ? Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE') : Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD');
|
||||
$html .= '</label>';
|
||||
$html .= '<input type="file" id="' . htmlspecialchars($id) . '-file"'
|
||||
. ' accept=".pem,.key,.openssh,.ppk,*" style="display:none;"'
|
||||
. ' onchange="mokoSshKeyFileSelected(\'' . htmlspecialchars($id) . '\', this)">';
|
||||
|
||||
$html .= '<span id="' . htmlspecialchars($id) . '-status" class="ms-2 text-muted small"></span>';
|
||||
|
||||
if ($hasKey) {
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-danger ms-2"'
|
||||
. ' onclick="mokoSshKeyClear(\'' . htmlspecialchars($id) . '\')">'
|
||||
. '<span class="icon-times" aria-hidden="true"></span> '
|
||||
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR')
|
||||
. '</button>';
|
||||
}
|
||||
|
||||
/* Hidden field — key data is NEVER rendered as visible text.
|
||||
On existing keys, we submit a sentinel value to preserve the DB value
|
||||
unless a new file is uploaded or clear is clicked. */
|
||||
if ($hasKey) {
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
||||
. ' value="__KEEP_EXISTING__">';
|
||||
} else {
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
||||
. ' value="">';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= $this->getScript();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getScript(): string
|
||||
{
|
||||
return <<<'JS'
|
||||
<script>
|
||||
function mokoSshKeyFileSelected(fieldId, input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
var file = input.files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
/* Base64 encode the key before storing in the hidden field */
|
||||
var content = e.target.result;
|
||||
var encoded = btoa(content);
|
||||
document.getElementById(fieldId).value = encoded;
|
||||
var status = document.getElementById(fieldId + '-status');
|
||||
if (status) status.textContent = file.name + ' uploaded';
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function mokoSshKeyClear(fieldId) {
|
||||
document.getElementById(fieldId).value = '';
|
||||
var status = document.getElementById(fieldId + '-status');
|
||||
if (status) status.textContent = 'Key removed';
|
||||
var fileInput = document.getElementById(fieldId + '-file');
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,23 @@ class ProfileTable extends Table
|
||||
|
||||
public function store($updateNulls = true): bool
|
||||
{
|
||||
/* Handle SSH key sentinel — when __KEEP_EXISTING__ is submitted,
|
||||
preserve the current DB value instead of overwriting with the sentinel.
|
||||
This prevents the key from being exposed in the form HTML. */
|
||||
if (isset($this->sftp_key_data) && $this->sftp_key_data === '__KEEP_EXISTING__') {
|
||||
if ($this->id) {
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('sftp_key_data'))
|
||||
->from($db->quoteName($this->_tbl))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $this->id);
|
||||
$db->setQuery($query);
|
||||
$this->sftp_key_data = $db->loadResult() ?: '';
|
||||
} else {
|
||||
$this->sftp_key_data = '';
|
||||
}
|
||||
}
|
||||
|
||||
$result = parent::store($updateNulls);
|
||||
|
||||
if ($result && !empty($this->backup_dir)) {
|
||||
|
||||
Reference in New Issue
Block a user