20261024 Seafile
1. Unauthenticated Remote Code Execution in the SeaDoc Converter (CVE-2026-129167)
SeaDoc consists of three processes. seahub handles authentication and page rendering, sdoc-server handles collaborative editing, and seadoc-converter converts documents. All three the same HMAC key, JWT_PRIVATE_KEY . When a visitor opens a public .sdoc link, seahub embeds a JWT token in the page so that anonymous preview works.
check_auth_token in the converter only verifies the signature. It checks neither the document the token was issued for nor the permission the token carries:
Once the signature is valid, check_auth_token returns True , so the anonymous token reaches every converter endpoint and the permission field of the payload has no effect.
The converter exposes /api/v1/confluence-to-wiki/ . That endpoint takes filename from the request body and uses it as a path:
When the second argument of os.path.join begins with a slash, the first argument is discarded. Both space_dir and zip_file_path thus become attacker-controlled absolute paths, and the converter creates the directory and unpacks the archive there as the user it runs under. With space_key set to /usr/lib/python3.12/encodings/cp273 , the __init__.py inside the archive lands in the encodings package as a subpackage named cp273 .
The converter cleans up after the extraction in a finally block, but only after the result upload returns. That upload has no timeout:
The clean-up is this block:
If upload_url points to an address that accepts the connection and never answers, requests.post blocks, finally never runs, and the planted files stay on disk.
A file on disk does not execute by itself. /api/v1/file-convert/ logs the upload response body when the upload fails:
Response.text evaluates str(self.content, encoding) , where encoding comes from the charset parameter of the response header. If the upload address answers 500 and declares charset=cp273 , CPython lazily imports the corresponding codec from the encodings package. FileFinder looks for the directory package /__init__.py before the module .py , so the import resolves to the planted encodings/cp273/__init__.py . Its code then runs inside the converter process, and no restart is needed.
Reproduction Environment
The lab runs the official Docker compose deployment:
Create an account, create a .sdoc file, and generate a public link for it.
the page source for seadocAccessToken and copy the string that follows it. This is the JWT:
The token is a read-only anonymous credential. Its payload is:
The converter downloads an archive from download_url and posts the result to upload_url . Run a service on a host the target can reach. It provides four endpoints: /payload.zip returns the archive to plant, /md returns the markdown to convert, /hang accepts the connection and never answers, and /err/ answers 500 with the given charset. The script is:
In the commands below, 192.0.2.10 stands for that host and 18099 for its port. Start the service:
Send a request to confluence-to-wiki with filename set to /usr/lib/python3.12/encodings/cp273aaaaaaaaa and upload_url pointing at /hang . The request does not return:
The files are on disk inside the seadoc container while the request stays blocked:
Then send a request to file-convert with upload_url pointing at /err/cp273 :
The command has run on the server:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
