mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-16 20:33:56 +00:00
Some checks are pending
Core / Core (HF=default + TRL=default) (push) Waiting to run
Core / Core (HF=4.57.6 + TRL<1) (push) Waiting to run
Core / Core (HF=latest + TRL=latest) (push) Waiting to run
Local Agent Guides CI / connection (codex) (push) Waiting to run
Local Agent Guides CI / connection (hermes) (push) Waiting to run
Local Agent Guides CI / connection (openclaw) (push) Waiting to run
Local Agent Guides CI / connection (opencode) (push) Waiting to run
Core / llama.cpp build + smoke (push) Waiting to run
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Waiting to run
Local Agent Guides CI / prompt-cache (gemma-3-270m) (push) Waiting to run
Local Agent Guides CI / connection (claude) (push) Waiting to run
Local Agent Guides CI / connection (pi) (push) Waiting to run
Local Agent Guides CI / file-edit (claude) (push) Waiting to run
Local Agent Guides CI / file-edit (codex) (push) Waiting to run
Local Agent Guides CI / resume (pi) (push) Waiting to run
Lockfile supply-chain audit / lockfile supply-chain audit (push) Waiting to run
Local Agent Guides CI / file-edit (hermes) (push) Waiting to run
Local Agent Guides CI / file-edit (openclaw) (push) Waiting to run
Local Agent Guides CI / file-edit (opencode) (push) Waiting to run
Local Agent Guides CI / file-edit (pi) (push) Waiting to run
Local Agent Guides CI / resume (claude) (push) Waiting to run
Local Agent Guides CI / resume (codex) (push) Waiting to run
Local Agent Guides CI / resume (opencode) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Security audit / pip scan-packages :: extras (push) Waiting to run
Security audit / pip scan-packages :: studio (push) Waiting to run
Security audit / pip scan-packages :: hf-stack (push) Waiting to run
Security audit / npm scan-packages (Unsloth frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Security audit / advisory audit (pip + npm + cargo) (push) Waiting to run
Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Unsloth export capability / capability (windows-latest) (push) Waiting to run
Unsloth export capability / capability (ubuntu-latest) (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Mac Studio GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI + API + Update CI / Chat UI, API and Update Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (windows) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
222 lines
8 KiB
Python
222 lines
8 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Browser regression for the desktop Python tool image boundary.
|
|
|
|
Runs as a standalone script. It serves a page with the exact Tauri CSP, then
|
|
checks that trusted code can fetch an authenticated sandbox image into a blob
|
|
URL while an allowed HTTPS image redirect cannot reach the HTTP Studio backend.
|
|
The same policy intentionally continues to allow ordinary HTTPS images,
|
|
including HTTPS loopback, which is outside this PR's HTTP-backend boundary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
from contextlib import contextmanager
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from typing import Iterator
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from _playwright_robust import chromium_launch_args # noqa: E402
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
TAURI_CONFIG = REPO / "studio/src-tauri/tauri.conf.json"
|
|
REDIRECT_URL = "https://redirect.invalid/attacker.png"
|
|
HTTPS_LOOPBACK_URL = "https://127.0.0.1:9443/sensitive/direct.png"
|
|
SANDBOX_PATH = "/api/inference/sandbox/session%20id/loss%20curve.png"
|
|
SENSITIVE_PATH = "/sensitive/redirect.png"
|
|
AUTHORIZATION = "Bearer browser-test-token"
|
|
PNG = base64.b64decode(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
|
)
|
|
|
|
|
|
class ProbeState:
|
|
def __init__(self) -> None:
|
|
self.paths: list[str] = []
|
|
self.authorization: str | None = None
|
|
|
|
|
|
class ProbeServer(ThreadingHTTPServer):
|
|
daemon_threads = True
|
|
|
|
def __init__(self, handler: type[BaseHTTPRequestHandler], state: ProbeState):
|
|
super().__init__(("127.0.0.1", 0), handler)
|
|
self.state = state
|
|
|
|
|
|
class TargetHandler(BaseHTTPRequestHandler):
|
|
server: ProbeServer
|
|
|
|
def log_message(self, format: str, *args: object) -> None:
|
|
del format, args
|
|
|
|
def _cors(self) -> None:
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
self.send_header("Access-Control-Allow-Headers", "Authorization")
|
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
|
|
def do_OPTIONS(self) -> None:
|
|
self.send_response(204)
|
|
self._cors()
|
|
self.end_headers()
|
|
|
|
def do_GET(self) -> None:
|
|
self.server.state.paths.append(self.path)
|
|
if self.path == SANDBOX_PATH:
|
|
self.server.state.authorization = self.headers.get("Authorization")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "image/png")
|
|
self.send_header("Content-Length", str(len(PNG)))
|
|
self._cors()
|
|
self.end_headers()
|
|
self.wfile.write(PNG)
|
|
return
|
|
|
|
self.send_response(204)
|
|
self.end_headers()
|
|
|
|
|
|
def page_handler(csp: str, target_origin: str) -> type[BaseHTTPRequestHandler]:
|
|
class PageHandler(BaseHTTPRequestHandler):
|
|
def log_message(self, format: str, *args: object) -> None:
|
|
del format, args
|
|
|
|
def do_GET(self) -> None:
|
|
if self.path == "/app.js":
|
|
script = f"""
|
|
const image = document.querySelector("#sandbox");
|
|
window.sandboxResult = (async () => {{
|
|
const response = await fetch("{target_origin}{SANDBOX_PATH}", {{
|
|
headers: {{ Authorization: "{AUTHORIZATION}" }},
|
|
}});
|
|
if (!response.ok) throw new Error(`sandbox fetch failed: ${{response.status}}`);
|
|
const blob = await response.blob();
|
|
window.sandboxObjectUrl = URL.createObjectURL(blob);
|
|
await new Promise((resolve, reject) => {{
|
|
image.addEventListener("load", resolve, {{ once: true }});
|
|
image.addEventListener("error", reject, {{ once: true }});
|
|
image.src = window.sandboxObjectUrl;
|
|
}});
|
|
return {{ width: image.naturalWidth, height: image.naturalHeight }};
|
|
}})();
|
|
window.revokeSandbox = () => URL.revokeObjectURL(window.sandboxObjectUrl);
|
|
"""
|
|
body = script.encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/javascript; charset=utf-8")
|
|
else:
|
|
body = f"""<!doctype html>
|
|
<html>
|
|
<body>
|
|
<img id="redirect" src="{REDIRECT_URL}" alt="remote">
|
|
<img id="https-loopback" src="{HTTPS_LOOPBACK_URL}" alt="https loopback">
|
|
<img id="sandbox" alt="loss curve.png">
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|
|
""".encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
self.send_header("Content-Security-Policy", csp)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
return PageHandler
|
|
|
|
|
|
@contextmanager
|
|
def running_server(server: ThreadingHTTPServer) -> Iterator[ThreadingHTTPServer]:
|
|
thread = threading.Thread(target = server.serve_forever, daemon = True)
|
|
thread.start()
|
|
try:
|
|
yield server
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout = 5)
|
|
|
|
|
|
def main() -> None:
|
|
config = json.loads(TAURI_CONFIG.read_text(encoding = "utf-8"))
|
|
csp = config["app"]["security"]["csp"]
|
|
state = ProbeState()
|
|
target = ProbeServer(TargetHandler, state)
|
|
target_origin = f"http://127.0.0.1:{target.server_port}"
|
|
page_server = ProbeServer(page_handler(csp, target_origin), ProbeState())
|
|
page_origin = f"http://127.0.0.1:{page_server.server_port}"
|
|
|
|
with running_server(target), running_server(page_server), sync_playwright() as p:
|
|
launch_options: dict[str, object] = {
|
|
"headless": True,
|
|
"args": chromium_launch_args(),
|
|
}
|
|
channel = os.environ.get("STUDIO_PLAYWRIGHT_CHANNEL")
|
|
if channel:
|
|
launch_options["channel"] = channel
|
|
browser = p.chromium.launch(**launch_options)
|
|
try:
|
|
context = browser.new_context()
|
|
redirect_requests = 0
|
|
https_loopback_requests = 0
|
|
|
|
def redirect(route) -> None:
|
|
nonlocal redirect_requests
|
|
redirect_requests += 1
|
|
route.fulfill(
|
|
status = 302,
|
|
headers = {"Location": f"{target_origin}{SENSITIVE_PATH}"},
|
|
)
|
|
|
|
def https_loopback(route) -> None:
|
|
nonlocal https_loopback_requests
|
|
https_loopback_requests += 1
|
|
route.fulfill(status = 200, content_type = "image/png", body = PNG)
|
|
|
|
context.route(REDIRECT_URL, redirect)
|
|
context.route(HTTPS_LOOPBACK_URL, https_loopback)
|
|
page = context.new_page()
|
|
page.goto(page_origin, wait_until = "domcontentloaded")
|
|
dimensions = page.evaluate("window.sandboxResult")
|
|
page.wait_for_timeout(500)
|
|
|
|
assert dimensions == {"width": 1, "height": 1}
|
|
assert state.authorization == AUTHORIZATION
|
|
assert SANDBOX_PATH in state.paths
|
|
assert redirect_requests == 1
|
|
assert SENSITIVE_PATH not in state.paths
|
|
assert https_loopback_requests == 1
|
|
assert page.locator("#https-loopback").evaluate(
|
|
"image => image.complete && image.naturalWidth === 1"
|
|
)
|
|
assert page.locator("#sandbox").get_attribute("src").startswith("blob:")
|
|
|
|
object_url = page.evaluate("window.sandboxObjectUrl")
|
|
image_loads = """url => new Promise((resolve) => {
|
|
const image = new Image();
|
|
image.addEventListener("load", () => resolve(true), { once: true });
|
|
image.addEventListener("error", () => resolve(false), { once: true });
|
|
image.src = url;
|
|
})"""
|
|
assert page.evaluate(image_loads, object_url)
|
|
|
|
page.evaluate("window.revokeSandbox()")
|
|
assert not page.evaluate(image_loads, object_url)
|
|
finally:
|
|
browser.close()
|
|
|
|
print("desktop Python tool image browser regression: PASS")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|