astroklient: strumień do warstwy logicznej też niesie pulę konta
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m1s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m30s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m24s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 21s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s

_post dokłada pulę z kontekstu sesji do każdego żądania, ale strumień
(używany przez /chart/horoscope/stream) szedł obok niego i wysyłał payload
bez zmian. Generowania w tym obrazie dziś nie ma, ale most modułów zostaje
celowo — po dołożeniu modułu horoskop strumieniowany szukałby w całym
udziale, a pole tenant podsunięte w payloadzie przeszłoby dalej.

Sonda w osobnym procesie przepuszcza prawdziwe żądanie przez bramkę sesji
do trasy w miejscu modułu (prompt, horoskop, strumień konsumowany leniwie)
i podsuwa cudzy login w payloadzie. Przed poprawką strumień wysyłał
„ktos-inny", po — konto z sesji.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 13:28:05 +02:00
parent 5c9989f05e
commit 4102039447
3 changed files with 69 additions and 4 deletions
@@ -74,7 +74,12 @@ class LogicClient:
"""Strumień NDJSON w dół — przekazywany dalej ramka po ramce.
Idzie przez szyfrowane łącze jak reszta ruchu (PRE-16): bez tego przy
włączonym LINK_ENCRYPTION_REQUIRED serwer odrzuciłby żądanie."""
włączonym LINK_ENCRYPTION_REQUIRED serwer odrzuciłby żądanie.
Nie przechodzi przez `_post`, więc pulę konta dokłada sam — tak samo,
z kontekstu i z pierwszeństwem przed polem przyniesionym w payloadzie.
Bez tego horoskop strumieniowany szukałby w całym udziale."""
payload = {**payload, "tenant": KONTO.get("")}
with httpx.Client(timeout=httpx.Timeout(None, connect=15.0)) as client:
yield from link_crypto.stream_lines(
client, f"{self.base_url}{sciezka}",
+54 -3
View File
@@ -80,15 +80,66 @@ print("WYNIK:" + json.dumps({
'''
@pytest.fixture(scope="module")
def wynik(drzewo):
# Generowania tekstu w tym obrazie nie ma, ale most modułów zostaje — więc jego
# transport musi nieść pulę z sesji tak samo jak wywołania ekranów. Osobna sonda,
# bo dokłada trasę, której w produkcie nie ma: w sondzie produktu zafałszowałaby
# każde pytanie o to, co produkt zawiera.
SONDA_MODULU = '''
import json, os, hashlib, secrets
sol = secrets.token_bytes(16)
dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32)
os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()),
SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0")
from fastapi.responses import StreamingResponse
from starlette.testclient import TestClient
from app.main import app, logic
from app import security, session, features
from app.clients import logic_client
wyslane = {}
def call_json(client, method, url, payload=None, **kw):
wyslane[url.split("/chart/", 1)[1]] = (payload or {}).get("tenant")
return {}
def stream_lines(client, url, payload=None, **kw):
wyslane[url.split("/chart/", 1)[1]] = (payload or {}).get("tenant")
yield '{"type": "result", "result": {}}'
logic_client.link_crypto.call_json = call_json
logic_client.link_crypto.stream_lines = stream_lines
# Trasa w miejscu modułu: woła ten sam transport co moduł generowania, a strumień
# konsumuje równie leniwie — dopiero przy wysyłaniu odpowiedzi, nie w obsłudze trasy.
features.ROUTES[("POST", "/sonda/modul")] = None
@app.post("/sonda/modul")
def sonda_modul():
podrobione = {"profile": "natal", "tenant": "ktos-inny"}
logic.wywolaj("/chart/prompt", podrobione, 5)
logic.wywolaj("/chart/horoscope", podrobione, 5)
def relay():
yield from logic.strumien("/chart/horoscope/stream", podrobione)
return StreamingResponse(relay(), media_type="application/x-ndjson")
c = TestClient(app, follow_redirects=False)
H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"}
r = c.post("/sonda/modul", headers=H)
print("WYNIK:" + json.dumps({"status": r.status_code, "pule": wyslane}))
'''
def _uruchom(drzewo: pathlib.Path, sonda: str) -> dict:
"""Aplikacja uruchomiona w OSOBNYM PROCESIE.
Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez
modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie
pozytywny."""
r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True,
r = subprocess.run([sys.executable, "-c", sonda], cwd=drzewo, capture_output=True,
text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"})
linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None)
assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}"
return json.loads(linia[len("WYNIK:"):])
@pytest.fixture(scope="module")
def wynik(drzewo):
return _uruchom(drzewo, SONDA)
@pytest.fixture(scope="module")
def wynik_modulu(drzewo):
return _uruchom(drzewo, SONDA_MODULU)
@@ -44,3 +44,12 @@ def test_the_pool_comes_from_the_session_not_from_the_form(wynik):
"""Pula jedzie z zalogowanego konta. Gdyby brała się z żądania, wystarczyłoby
podstawić cudzy login, żeby czytać cudze pliki — sonda właśnie to próbuje."""
assert wynik["pula"] == "klient"
def test_module_transport_carries_the_session_pool_too(wynik_modulu):
"""Prompt i horoskop idą mostem modułów, a strumień horoskopu obok `_post` —
i właśnie tam pula ginęła, więc horoskop szukałby w całym udziale. Sonda
podsuwa cudzy login w payloadzie; ma wygrać konto z sesji."""
assert wynik_modulu["status"] == 200
assert wynik_modulu["pule"] == {"prompt": "klient", "horoscope": "klient",
"horoscope/stream": "klient"}