security: redirect_uri-Allowlist + mail Path-Traversal-Schutz
- common.py /authorize: redirect_uri muss https://claude.ai/ oder https://claude.com/ sein (verhindert offenen Redirect / Code-Abfluss an fremde URIs). - mail/server.py _open_folder: realpath-Check, Ordner muss im Account-Verzeichnis bleiben (Path-Traversal verhindert). - tests: get_token_pkce auf sicheren Flow aktualisiert (erlaubte redirect_uri + client_secret). 54/54 Tests gruen. test-Client bleibt (Test-Suite braucht ihn, Secret 600-geschuetzt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,9 @@ async def oauth_authorize(request: Request):
|
|||||||
if not code_challenge:
|
if not code_challenge:
|
||||||
return HTMLResponse("<h1>Fehler</h1><p>PKCE erforderlich (code_challenge fehlt).</p>", status_code=400)
|
return HTMLResponse("<h1>Fehler</h1><p>PKCE erforderlich (code_challenge fehlt).</p>", status_code=400)
|
||||||
|
|
||||||
|
if not redirect_uri.startswith(("https://claude.ai/", "https://claude.com/")):
|
||||||
|
return HTMLResponse("<h1>Fehler</h1><p>Ungueltige redirect_uri.</p>", status_code=400)
|
||||||
|
|
||||||
code = secrets.token_urlsafe(32)
|
code = secrets.token_urlsafe(32)
|
||||||
_auth_codes[code] = {
|
_auth_codes[code] = {
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
|
|||||||
+5
-1
@@ -161,7 +161,11 @@ def _discover_folders(acct_path):
|
|||||||
|
|
||||||
|
|
||||||
def _open_folder(acct_path, folder_name):
|
def _open_folder(acct_path, folder_name):
|
||||||
path = os.path.join(acct_path, folder_name)
|
base = os.path.realpath(acct_path)
|
||||||
|
path = os.path.realpath(os.path.join(base, folder_name))
|
||||||
|
# Path-Traversal verhindern: muss innerhalb des Account-Verzeichnisses bleiben
|
||||||
|
if path != base and not path.startswith(base + os.sep):
|
||||||
|
return None
|
||||||
return mailbox.Maildir(path, create=False) if os.path.isdir(path) else None
|
return mailbox.Maildir(path, create=False) if os.path.isdir(path) else None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -48,12 +48,12 @@ def get_token_pkce(port):
|
|||||||
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
|
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
|
||||||
r = httpx.get(f"http://127.0.0.1:{port}/authorize", params={
|
r = httpx.get(f"http://127.0.0.1:{port}/authorize", params={
|
||||||
"response_type": "code", "client_id": "test",
|
"response_type": "code", "client_id": "test",
|
||||||
"redirect_uri": "http://localhost/cb", "code_challenge": challenge, "code_challenge_method": "S256",
|
"redirect_uri": "https://claude.ai/api/mcp/auth_callback", "code_challenge": challenge, "code_challenge_method": "S256",
|
||||||
}, follow_redirects=False, timeout=10)
|
}, follow_redirects=False, timeout=10)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
code = r.headers["location"].split("code=")[1].split("&")[0]
|
code = r.headers["location"].split("code=")[1].split("&")[0]
|
||||||
r2 = httpx.post(f"http://127.0.0.1:{port}/token", data={
|
r2 = httpx.post(f"http://127.0.0.1:{port}/token", data={
|
||||||
"grant_type": "authorization_code", "code": code, "client_id": "test", "code_verifier": verifier,
|
"grant_type": "authorization_code", "code": code, "client_id": "test", "client_secret": TEST_SECRET, "code_verifier": verifier,
|
||||||
}, timeout=10)
|
}, timeout=10)
|
||||||
assert r2.status_code == 200
|
assert r2.status_code == 200
|
||||||
return r2.json()["access_token"]
|
return r2.json()["access_token"]
|
||||||
|
|||||||
Reference in New Issue
Block a user