Zbyt permisywna polityka CORS w LoopX serve-status ujawnia stan płaszczyzny sterowania i prywatne pliki Markdown
Podsumowanie
loopx serve-status wysyła Access-Control-Allow-Origin: * w każdej odpowiedzi, w tym na nieuwierzytelnionych trasach GET. Ponieważ punkt końcowy nie wymaga uwierzytelnienia i jest powiązany ze stałym, powszechnie znanym portem pętli zwrotnej (loopback), każda witryna odwiedzana przez operatora może odczytać stan płaszczyzny sterowania LoopX oraz zawartość plików Markdown w każdym zarejestrowanym repozytorium celu, używając zwykłego fetch() bez jakiejkolwiek interakcji ze strony użytkownika.
Punkty końcowe zapisu nie są dotknięte problemem — poprawnie weryfikują nagłówek Origin. Niniejsze zgłoszenie dotyczy wyłącznie ścieżek odczytu.
Wpływ
Gdy loopx serve-status jest uruchomiony, strona z dowolnego pochodzenia (origin) może wykonać:
// no interaction, no preflight, no credentials needed
const s = await (await fetch('http://127.0.0.1:8766/status.json')).json();
for (const g of s.goals ?? []) {
const m = await (await fetch(
`http://127.0.0.1:8766/review-material?goal_id=${g.id}&path=design-notes.md`
)).json();
exfiltrate(m.content);
}
i odczytać:
GET /status.json— pełną projekcję stanu: identyfikatory celów, historię uruchomień oraz lokalne ścieżki bezwzględne (registry,runtime_root). Z flagą--global-registryobejmuje to każdy projekt zarejestrowany na maszynie, a nie tylko jeden.GET /review-material?goal_id=…&path=…— zawartość dowolnego pliku.md/.markdownznajdującego się w katalogu głównym repozytorium celu, katalogu stanu lub katalogu uruchomieniowym. W praktyce oznacza to prywatne notatki projektowe operatora, dokumentację przekazania i raporty dowodowe.GET /— wykrywanie punktów końcowych, w tym informację o włączonych interfejsach API zapisu.POST /control-plane/configure-goal/dry-run— ta trasa nie posiada żadnej weryfikacji Origin, ado_OPTIONSzwracaAccess-Control-Allow-Headers: Content-Type, więc żądanie preflight dla cross-origin JSON POST kończy się powodzeniem. Odpowiedź ujawnia konfigurację płaszczyzny sterowaniabefore/afterdla danego celu. (Nie wykonuje operacji zapisu — zweryfikowanowritten=false).
docs/public-private-boundary.md klasyfikuje „lokalne ścieżki bezwzględne”, „identyfikatory zadań”, „surowe logi i metryki” oraz „aktywny stan celu ujawniający bieżący kontekst użytkownika” jako materiały, które muszą pozostać prywatne. Wszystkie te dane są obecnie czytelne dla dowolnego pochodzenia.
Dwa czynniki sprawiają, że zagrożenie nie jest jedynie teoretyczne:
- Stałe porty domyślne.
8765(projekt) oraz8766(globalny) to zakodowane domyślne porty, więc strona atakującego może po prostu wypróbować oba — faza wykrywania nie jest potrzebna. - Jest udokumentowane jako usługa działająca w tle.
docs/guides/getting-started.mddokumentujeloopx serve-status, ascripts/macos-dashboard-launchagent.shinstaluje LaunchAgent, który utrzymuje działanieserve-status --global-registry --port 8766po każdym logowaniu. Dla każdego, kto postępuje zgodnie z tym przewodnikiem, stanowi to stale aktywną, dostępną na poziomie całej maszyny powierzchnię odczytu.
Problem oceniono jako High ze względu na bezinterakcyjną eksfiltrację prywatnych danych lokalnych z trwałej usługi. Dotyczy wyłącznie odczytu i ma ograniczony zakres.
Lokalizacja
loopx/status_server.py:136-146 — StatusRequestHandler._send_json():
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # <-- line 142
_send_json jest wspólną ścieżką odpowiedzi dla wszystkich tras, dlatego symbol wieloznaczny (wildcard) jest stosowany zarówno do /status.json, /review-material, /healthz, /, jak i do zapytań POST w trybie testowym (dry-run).
do_OPTIONS (:148-153) powiela to zachowanie.
Potwierdzono występowanie w main HEAD (b0c8e315, blob c9e53d1a) w momencie zgłoszenia.
Co NIE podlega podatności
Zweryfikowano następujące mechanizmy kontrolne:
- Operacje zapisu są poprawnie zabezpieczone. Przy włączonych obu opcjach
--enable-reward-write-apioraz--enable-control-plane-write-api, trasyPOST /reward/appendiPOST /control-plane/configure-goal/applyzwracają 403 „only accepts loopback browser origins” dla obcegoOrigin.is_loopback_origin()działa poprawnie. Nie ma możliwości zapisu między źródłami (cross-origin), a atak DNS rebinding również nie omija zabezpieczeń (przepięta strona nadal niesie własny, nieloopbackowy origin). - Brak możliwości path traversal.
../../../../etc/hostname→ 400; bezwzględna ścieżka.mdpoza korzeniami celu → 400 („outside the goal repo/state/runtime roots”).path_is_under()działa poprawnie. - Biała lista rozszerzeń działa.
secrets.envwewnątrz repozytorium → 400. Odczytać można wyłącznie.md/.markdown. - Tryb testowy nie modyfikuje danych.
configure_goal_with_global_sync(execute=False)zwraca odpowiedź przed jakimkolwiek zapisem; zaobserwowanowritten=false.
Narażenie ogranicza się więc do: formatu JSON stanu + plików Markdown w katalogach zarejestrowanych celów. Są to nadal materiały prywatne, lecz podatność nie pozwala na odczyt dowolnych plików ani na zdalne wykonanie kodu.
Dowód koncepcji (PoC)
Uruchomiono na niezmodyfikowanym klonie w HEAD z domyślnymi flagami (brak włączonych API zapisu):
GET /healthz Origin: https://evil.example -> 200 ACAO: '*'
GET / Origin: https://evil.example -> 200 ACAO: '*'
GET /status.json Origin: https://evil.example -> 200 ACAO: '*'
GET /review-material?goal_id=victim-goal&path=internal-design-notes.md
Origin: https://evil.example -> 200 ACAO: '*'
content: "# Internal notes\n\nPROD_DB_HOST=db.internal.corp\nCONFIDENTIAL: ..."
Zawartość pliku została zwrócona dosłownie do obcego pochodzenia. Wyniki testów z tego samego środowiska z włączonymi obiema flagami zapisu:
POST /reward/append Origin: https://evil.example -> 403 (origin check)
POST /control-plane/configure-goal/apply Origin: https://evil.example -> 403 (origin check)
GET /review-material ../../../../etc/hostname -> 400 (traversal blocked)
GET /review-material <abs path outside roots>.md -> 400 (root check)
GET /review-material secrets.env -> 400 (extension check)
GET /status.json Origin: https://evil.example -> 200 ACAO '*' <-- still readable
Środowisko testowe to skrypt w standardowym Pythonie (~120 linii) niemodyfikujący plików w repozytorium.
Sugerowana poprawka
W pliku istnieje już poprawny predykat — is_loopback_origin(). Minimalna poprawka polega na zaprzestaniu emisji symbolu wieloznacznego i zwracaniu żądanego nagłówka Origin tylko wtedy, gdy przejdzie weryfikację:
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
origin = self.headers.get("Origin")
if origin and is_loopback_origin(origin):
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Vary", "Origin")
# no Origin header (curl, the CLI, a same-origin fetch) -> send no ACAO at all
self.end_headers()
self.wfile.write(body)
oraz to samo w do_OPTIONS. Pozwala to na działanie panelu — jest on serwowany z http://127.0.0.1:5174 i pobiera zasoby z 127.0.0.1:8765/8766, co stanowi komunikację cross-origin między dwoma portami loopback, jednocześnie odrzucając każde pochodzenie spoza pętli zwrotnej.
Dwa opcjonalne kroki utwardzające:
- Zastosowanie istniejącej weryfikacji
is_loopback_origin()również doPOST /control-plane/configure-goal/dry-runorazPOST /reward/dry-run, aby rzut konfiguracji nie był czytelny w trybie cross-origin. - Dodanie weryfikacji nagłówka
Host(odrzucanieHost, który nie wskazuje na loopback), aby uniemożliwić DNS rebinding klientom niewysyłającymOrigin.
Zakres audytu
Przejrzano punkty wejścia zidentyfikowane jako 26/52 w obrębie 758 plików (360,618 LOC) pakietu loopx i panelu kontrolnego; narzędzia: semgrep 1.172.0, trufflehog 3.96.0, osv-scanner 2.4.0, oraz manualna analiza przepływu danych. Pominięto: tests/, regression/, examples/, docs/ oraz adaptery testów porównawczych w loopx/benchmark_adapters/. Poziom pewności pokrycia: częściowy.
Inne elementy, w których nie stwierdzono problemów: automatyczna aktualizacja (curl | bash przez HTTPS pod stały adres URL raw.githubusercontent.com; nadpisanie LOOPX_ARCHIVE_URL wymaga kontroli środowiska, czyli uprawnień, które atakujący musiałby już posiadać); instalacja umiejętności komend projektu/ukośnika (ścisły regex identyfikatora plus ograniczenie _assert_project_boundary()); cykl roboczy procesora shell=True (wartość jest z założenia poleceniem powłoki skonfigurowanym przez użytkownika, a demo_supervisor.build_visible_worker_turn_command przekazuje cel przez shlex.quote); wychodzące podatności SSRF (content_ops/surface.py blokuje zakresy prywatne/loopback/link-local i odrzuca przekierowania). Brak pickle, yaml.load, eval ani os.system w całym pakiecie.
Doradztwa dotyczące zależności 3 występują wyłącznie w narzędziach dev/docs (pymdown-extensions, idna, postcss); żadne nie są osiągalne z poziomu pakietu runtime, który nie deklaruje zależności.
Wykryte przez
Aeon,) — zautomatyzowany audyt bezpieczeństwa. Ustalenia odtworzone na HEAD i ręcznie zweryfikowane przed zgłoszeniem. Zgłoszono prywatnie przez GitHub PVR zgodnie ze wskazaniem w SECURITY.md.
Co trzeba zrobić
Zaleca się aktualizację pakietu loopx do wersji 0.4.5 lub nowszej.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Python (PyPI) | loopx | >= 0.0.1, < 0.4.5 | 0.4.5 |
Odnośniki
- https://github.com/huangruiteng/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
- https://github.com/loopx-project/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
- https://nvd.nist.gov/vuln/detail/CVE-2026-108372
- https://github.com/loopx-project/loopx/pull/3137
- https://github.com/loopx-project/loopx/commit/838720fba42bd22f71b5bb763ec406f7bdf4c940
- https://github.com/huangruiteng/loopx
- https://github.com/loopx-project/loopx/releases/tag/v0.4.5
- Wektor CVSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N- Słabość (CWE)
CWE-346, CWE-942- Platforma
- Python (PyPI)
- Paczka
loopx- Ujawniono
- 19.10.2026
- Aktualizacja
- 20.10.2026 09:52
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-vx2m-gpq4-8j5q - CC BY 4.0
Historia podatności: loopx
Wszystkie opisane podatności tej paczki (Python (PyPI)), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.
To jedyna znana podatność tej paczki.
Dowiedz się, które podatności dotyczą Twojej aplikacji
Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.