Skip to content

Commit 5859df3

Browse files
committed
fix(playground): add CSRF token and Host/Origin checks to protect local API from cross-origin abuse
1 parent 6490453 commit 5859df3

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

packages/moss-cli/src/moss_cli/commands/playground.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import asyncio
1111
import json
12+
import secrets
1213
import socket
1314
import webbrowser
1415
from http.server import HTTPServer, SimpleHTTPRequestHandler
@@ -50,10 +51,26 @@ class PlaygroundHandler(SimpleHTTPRequestHandler):
5051
so the project key is never exposed to the browser."""
5152

5253
client: MossClient | None = None
54+
_token: str = ""
55+
_server_host: str = ""
5356

5457
def __init__(self, *args, **kwargs):
5558
super().__init__(*args, directory=str(HERE / "playground"), **kwargs)
5659

60+
def _check_api_request(self) -> bool:
61+
if self.headers.get("X-Moss-Token") != self._token:
62+
self._send_json(403, {"error": "Forbidden: invalid or missing token"})
63+
return False
64+
host = self.headers.get("Host", "")
65+
if host and host != self._server_host and host != self._server_host.replace("127.0.0.1", "localhost"):
66+
self._send_json(403, {"error": "Forbidden: invalid Host header"})
67+
return False
68+
origin = self.headers.get("Origin", "")
69+
if origin and origin != f"http://{self._server_host}":
70+
self._send_json(403, {"error": "Forbidden: invalid Origin"})
71+
return False
72+
return True
73+
5774
def _send_json(self, status: int, data: dict) -> None:
5875
body = json.dumps(data).encode("utf-8")
5976
self.send_response(status)
@@ -81,8 +98,12 @@ def do_GET(self) -> None:
8198
self.send_response(204)
8299
self.end_headers()
83100
elif path == "/api/indexes":
101+
if not self._check_api_request():
102+
return
84103
self._handle_list_indexes()
85104
elif path == "/api/index":
105+
if not self._check_api_request():
106+
return
86107
names = params.get("name", [])
87108
self._handle_get_index(names[0] if names else None)
88109
else:
@@ -93,6 +114,10 @@ def _serve_index(self) -> None:
93114
self._send_json(500, {"error": "Playground HTML not found"})
94115
return
95116
html = PLAYGROUND_HTML.read_text(encoding="utf-8")
117+
html = html.replace(
118+
"</title>",
119+
f'</title>\n<meta name="moss-token" content="{self._token}" />',
120+
)
96121
self._send_html(html)
97122

98123
def _handle_list_indexes(self) -> None:
@@ -129,6 +154,8 @@ def log_message(self, format, *args):
129154
console.print(f" [dim]{args[0]}[/dim]")
130155

131156
def do_POST(self) -> None:
157+
if not self._check_api_request():
158+
return
132159
parsed = urlparse(self.path)
133160
path = parsed.path
134161
length = int(self.headers.get("Content-Length", 0))
@@ -238,6 +265,8 @@ def playground_command(
238265
server_addr = ("127.0.0.1", final_port)
239266

240267
PlaygroundHandler.client = client
268+
PlaygroundHandler._token = secrets.token_urlsafe(32)
269+
PlaygroundHandler._server_host = f"127.0.0.1:{final_port}"
241270

242271
server = HTTPServer(server_addr, PlaygroundHandler)
243272
url = f"http://127.0.0.1:{final_port}"

packages/moss-cli/src/moss_cli/playground/index.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ <h1>Moss <span>Playground</span></h1>
135135
</div>
136136

137137
<script type="module">
138+
const mossToken = document.querySelector('meta[name="moss-token"]')?.getAttribute('content') || '';
139+
140+
function apiFetch(path, options = {}) {
141+
const headers = { ...options.headers, 'X-Moss-Token': mossToken };
142+
return fetch(path, { ...options, headers });
143+
}
144+
138145
let indexes = [];
139146
let currentIndex = null;
140147
let requestId = 0;
@@ -153,7 +160,7 @@ <h1>Moss <span>Playground</span></h1>
153160

154161
async function fetchIndexes() {
155162
try {
156-
const res = await fetch('/api/indexes');
163+
const res = await apiFetch('/api/indexes');
157164
if (!res.ok) throw new Error('Failed to fetch indexes');
158165
const data = await res.json();
159166
indexes = data.indexes || [];
@@ -227,7 +234,7 @@ <h1>Moss <span>Playground</span></h1>
227234
loadStatus.textContent = 'Connecting to cloud...';
228235

229236
try {
230-
const res = await fetch('/api/load-index', {
237+
const res = await apiFetch('/api/load-index', {
231238
method: 'POST',
232239
headers: { 'Content-Type': 'application/json' },
233240
body: JSON.stringify({ name }),
@@ -342,7 +349,7 @@ <h1>Moss <span>Playground</span></h1>
342349
if (placeholder) placeholder.innerHTML = '<p>Searching...</p>';
343350

344351
try {
345-
const res = await fetch('/api/query', {
352+
const res = await apiFetch('/api/query', {
346353
method: 'POST',
347354
headers: { 'Content-Type': 'application/json' },
348355
body: JSON.stringify({ name: currentIndex, query, topK, alpha }),

0 commit comments

Comments
 (0)