Check for existing issues
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
Check for existing issues
Describe the bug / provide steps to reproduce it
What
When you add an FTP or SFTP server, Acode stores the connection in
localStorage.storageListwiththe username and password embedded directly in the URL, in cleartext.
The URL is built in
src/utils/Url.js(formate()):and the resulting record is saved to
localStorageinsrc/lib/remoteStorage.js(addFtparoundline 58,
addSftparound line 238):So
localStorage.storageListends up holding entries like: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:
adb backup, alost/stolen device, or forensic extraction.
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 removesthat 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.xmluses AndroidX Security Crypto (
EncryptedSharedPreferences, Tink). So the app already has anencrypted-storage mechanism; the remote-server credentials just aren't using it.
Suggested fix
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.storageListblob at rest with a Keystore-backed key instead of writingit as plain JSON.
Happy to help test a change if that's useful.
Version
Checked on v1.12.6 and current
main.Environment
If applicable, add mockups / screenshots regarding your vision
No response
If applicable, attach your Acode.log file to this issue.
Acode.log