security: FTP password not masked in AjaxController maskSecrets/mergeExistingSecrets #169

Closed
opened 2026-06-29 14:20:05 +00:00 by jmiller · 2 comments
Owner

Summary

AjaxController::maskSecrets() and AjaxController::mergeExistingSecrets() define secret field mappings for sftp, s3, and google_drive remote types — but ftp is completely missing. When a remote destination of type ftp is loaded via the AJAX listRemotes endpoint, the FTP password is returned in cleartext to the browser.

Location

src/Controller/AjaxController.php — lines 1142-1146 and 1164-1168:

$secrets = [
    'sftp'         => ['password', 'passphrase', 'key_data'],
    's3'           => ['secret_key'],
    'google_drive' => ['client_secret', 'refresh_token'],
    // 'ftp' is missing — password returned in cleartext
];

What to do

  • Add 'ftp' => ['password'] to the $secrets array in maskSecrets()
  • Add 'ftp' => ['password'] to the $secrets array in mergeExistingSecrets()
  • Verify the API controller's profiles() endpoint also masks FTP credentials (it does via a separate $sensitiveFields list that includes ftp_password)

Why

This is a credential exposure vulnerability. Any admin user who can view the remotes list sees FTP passwords in the DOM/network response. The other three remote types correctly mask their secrets.

## Summary `AjaxController::maskSecrets()` and `AjaxController::mergeExistingSecrets()` define secret field mappings for `sftp`, `s3`, and `google_drive` remote types — but **`ftp` is completely missing**. When a remote destination of type `ftp` is loaded via the AJAX `listRemotes` endpoint, the FTP password is returned in cleartext to the browser. ## Location `src/Controller/AjaxController.php` — lines 1142-1146 and 1164-1168: ```php $secrets = [ 'sftp' => ['password', 'passphrase', 'key_data'], 's3' => ['secret_key'], 'google_drive' => ['client_secret', 'refresh_token'], // 'ftp' is missing — password returned in cleartext ]; ``` ## What to do - [ ] Add `'ftp' => ['password']` to the `$secrets` array in `maskSecrets()` - [ ] Add `'ftp' => ['password']` to the `$secrets` array in `mergeExistingSecrets()` - [ ] Verify the API controller's `profiles()` endpoint also masks FTP credentials (it does via a separate `$sensitiveFields` list that includes `ftp_password`) ## Why This is a credential exposure vulnerability. Any admin user who can view the remotes list sees FTP passwords in the DOM/network response. The other three remote types correctly mask their secrets.
jmiller added the component: remote label 2026-06-29 14:20:05 +00:00
Author
Owner

Branch created: feature/169-security-ftp-password-not-masked-in-ajax

git fetch origin
git checkout feature/169-security-ftp-password-not-masked-in-ajax
Branch created: [`feature/169-security-ftp-password-not-masked-in-ajax`](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/src/branch/feature/169-security-ftp-password-not-masked-in-ajax) ```bash git fetch origin git checkout feature/169-security-ftp-password-not-masked-in-ajax ```
Author
Owner

Confirmed against 02.56.05 — keep open (canonical FTP-masking issue; #185 closed as duplicate).

AjaxController.php:1203-1207 (maskSecrets()) and :1225-1229 (mergeExistingSecrets()) both build a $secrets map with only sftp / s3 / google_drive — no ftp key. FTP was removed from the create UI (tmpl/profile/edit.php:139-142 offers only those three), but RemoteTable.php:33 still lists 'ftp' in $validTypes, so an FTP remote can still be stored (legacy data or a crafted POST). When such a row is loaded via listRemotesmaskSecrets($config, $row->type) it falls through to $secrets['ftp'] ?? [] = empty, and the FTP password is returned unmasked.

Fix: add 'ftp' => ['password'] to both $secrets arrays. (Separately consider dropping 'ftp' from RemoteTable::$validTypes to close it at the data layer.)

Confirmed against 02.56.05 — keep open (canonical FTP-masking issue; #185 closed as duplicate). `AjaxController.php:1203-1207` (`maskSecrets()`) and `:1225-1229` (`mergeExistingSecrets()`) both build a `$secrets` map with only `sftp` / `s3` / `google_drive` — no `ftp` key. FTP was removed from the create UI (`tmpl/profile/edit.php:139-142` offers only those three), **but** `RemoteTable.php:33` still lists `'ftp'` in `$validTypes`, so an FTP remote can still be stored (legacy data or a crafted POST). When such a row is loaded via `listRemotes` → `maskSecrets($config, $row->type)` it falls through to `$secrets['ftp'] ?? []` = empty, and the FTP password is returned unmasked. Fix: add `'ftp' => ['password']` to both `$secrets` arrays. (Separately consider dropping `'ftp'` from `RemoteTable::$validTypes` to close it at the data layer.)
Sign in to join this conversation.