Skip to content

Remote server passwords (FTP/SFTP) are stored in cleartext in localStorage #2561

Description

@MYounas126

Check for existing issues

  • Completed

Describe the bug / provide steps to reproduce it

What

When you add an FTP or SFTP server, Acode stores the connection in localStorage.storageList with
the username and password embedded directly in the URL, in cleartext.

The URL is built in src/utils/Url.js (formate()):

// src/utils/Url.js:234
if (username && password) string += `${enc(username)}:${enc(password)}@`;

and the resulting record is saved to localStorage in src/lib/remoteStorage.js (addFtp around
line 58, addSftp around line 238):

const res = { url, alias, name: alias, type: "ftp", home: null };
// url = ftp://user:password@host:port/...

So localStorage.storageList ends up holding entries like:

sftp://admin:SuperSecret123@10.0.0.5:22/
ftp://deploy:hunter2@example.com:21/

localStorage for the WebView is stored unencrypted on disk (the Chromium leveldb under
app_webview/Default/Local Storage/), so these passwords sit in plaintext at rest.

Why it's worth fixing

I want to be clear about the threat model: on a normal, non-rooted device another app can't read
this, so this isn't remotely exploitable on its own. It matters as defense-in-depth:

  • Anything with local access reads it in cleartext — a rooted/compromised device, adb backup, a
    lost/stolen device, or forensic extraction.
  • Any future code-execution bug in the WebView gets the credentials for free. (This is exactly what
    happened with the Find-File XSS in GHSA-x24x-425w-326q — once JS ran in the WebView, one
    localStorage.getItem("storageList") returned every saved password. Fixing the storage removes
    that payoff even if another injection shows up later.)

The part that makes this an easy call

Acode already does the right thing for its own account token — shared_prefs/acode_auth_secure.xml
uses AndroidX Security Crypto (EncryptedSharedPreferences, Tink). So the app already has an
encrypted-storage mechanism; the remote-server credentials just aren't using it.

Suggested fix

  • Don't keep the password in the URL string in localStorage. Store the non-secret parts
    (host, port, username, alias) where they are, and keep the password/passphrase/key in
    EncryptedSharedPreferences (or the Android Keystore), the same way the account token is handled.
  • Or, minimally, encrypt the storageList blob at rest with a Keystore-backed key instead of writing
    it as plain JSON.

Happy to help test a change if that's useful.

Version

Checked on v1.12.6 and current main.

Environment

  • Acode version: 1.12.6 (versionCode 1001)
  • Android version: 17 (API 37)
  • WebView: Chrome 145.0.7632.218
  • Device: emulator (sdk_gphone64_arm64)

If applicable, add mockups / screenshots regarding your vision

No response

If applicable, attach your Acode.log file to this issue.

Acode.log

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions