From 71a6517073edbfe32d66dad34dcda72f39d3396e Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:02:28 +0200 Subject: [PATCH] Fix non-ASCII token authentication crashes --- atelier/app.py | 6 ++++-- tests/test_app.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/atelier/app.py b/atelier/app.py index 400ebde..3226b06 100644 --- a/atelier/app.py +++ b/atelier/app.py @@ -199,7 +199,9 @@ def create_app(data_dir=None, provider_transport=None): return JSONResponse({'detail': 'Fremde Herkunft nicht erlaubt.'}, status_code=403) if path == '/mcp' or path.startswith('/mcp/'): supplied = request.headers.get('authorization', '') - if not secrets.compare_digest(supplied, 'Bearer ' + mcp_token): + # ASGI headers are decoded as Latin-1; compare their original bytes. + # compare_digest(str, str) rejects non-ASCII characters. + if not secrets.compare_digest(supplied.encode('latin-1'), ('Bearer ' + mcp_token).encode('utf-8')): return JSONResponse({'detail': 'MCP-Token erforderlich.'}, status_code=401) elif path.startswith('/api/') and path != '/api/login': session = request.cookies.get('atelier_session', '') @@ -237,7 +239,7 @@ def create_app(data_dir=None, provider_transport=None): attempts[ip] = recent if len(recent) >= 10: raise HTTPException(429, 'Zu viele Versuche. Bitte eine Minute warten.') - if not secrets.compare_digest(body.token, admin_token): + if not secrets.compare_digest(body.token.encode('utf-8'), admin_token.encode('utf-8')): recent.append(stamp) raise HTTPException(401, 'Zugangsschlüssel stimmt nicht.') for key in list(sessions): diff --git a/tests/test_app.py b/tests/test_app.py index 704e1d9..a9aeeea 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -181,3 +181,27 @@ def test_german_metadata_is_a_proposal_only(client): assert response.json()['title'] == 'Kundenanfrage höflich absagen' assert set(response.json()) == {'title','description','category','tags'} assert client.get('/api/prompts').json() == before + + +@pytest.mark.parametrize('configured', ['ascii-test-token', 'synthetic-ü-token-🔐']) +def test_mcp_non_ascii_tokens_never_crash(tmp_path, monkeypatch, configured): + monkeypatch.setenv('MCP_TOKEN', configured) + with TestClient(create_app(tmp_path)) as c: + initialize={'jsonrpc':'2.0','id':1,'method':'initialize','params':{'protocolVersion':'2025-03-26','capabilities':{},'clientInfo':{'name':'regression','version':'1'}}} + accept={'Accept':'application/json, text/event-stream'} + for path in ['/mcp', '/mcp/']: + for wrong in [b'', b'Bearer wrong', 'Bearer falsch-ä'.encode('utf-8'), b'Bearer \xff']: + assert c.post(path,headers={**accept,'Authorization':wrong},json=initialize).status_code == 401 + headers={**accept,'Authorization':('Bearer '+configured).encode('utf-8')} + response=c.post(path,headers=headers,json=initialize) + assert response.status_code == 200 + assert response.json()['result']['serverInfo']['name'] == 'CasaDePrompt' + listed=c.post(path,headers=headers,json={'jsonrpc':'2.0','id':2,'method':'tools/list','params':{}}) + assert len(listed.json()['result']['tools']) == 4 + + +def test_web_login_with_unicode_token(tmp_path, monkeypatch): + monkeypatch.setenv('ADMIN_TOKEN', 'synthetic-ä-admin-🔐') + with TestClient(create_app(tmp_path)) as c: + assert c.post('/api/login',json={'token':'falsch-ü'}).status_code == 401 + assert c.post('/api/login',json={'token':'synthetic-ä-admin-🔐'}).status_code == 200