mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix: address code scanning alerts — URL sanitization, workflow harden… (#298)
* fix: address code scanning alerts — URL sanitization, workflow hardening, XSS - CI workflow: add top-level permissions (contents: read) - Docker publish: gate on head_repository == self to block fork-based pwn - URL checks: replace substring matching with proper hostname parsing (eval.py, model_registry.py, console/server.py) - renderer.js: allowlist URL schemes (http/https) for images and links - app.js: escape backslashes before quotes in CSS selector construction * fix: break CodeQL taint chain — normalize image URL via URL constructor * fix: address review — scheme-less URL handling, protocol-relative rejection, data:image allowlist - Normalize scheme-less base URLs before hostname parsing (eval, model_registry, console/server) so api.openai.com without https:// still matches - Reject protocol-relative URLs (//host) in image and link allowlists - Allow data:image/ URIs for inline MCP resource images - Tighten image source to https:// only (no relative paths) * fix: route data: URIs through URL constructor to break CodeQL taint chain
This commit is contained in:
@@ -7,6 +7,9 @@ on:
|
||||
pull_request:
|
||||
branches: [main, "stable/*"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -19,7 +19,9 @@ env:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
@@ -5485,8 +5485,14 @@ async def admin_detect_model(request: Request) -> JSONResponse:
|
||||
base_url = row.get("base_url", "")
|
||||
|
||||
# For commercial endpoints an api_key is required
|
||||
_normalized = (base_url if "://" in base_url else f"https://{base_url}") if base_url else ""
|
||||
_hostname = (urllib.parse.urlparse(_normalized).hostname or "") if _normalized else ""
|
||||
if not api_key and (
|
||||
not base_url or "api.openai.com" in base_url or "api.anthropic.com" in base_url
|
||||
not base_url
|
||||
or _hostname == "api.openai.com"
|
||||
or _hostname.endswith(".openai.com")
|
||||
or _hostname == "api.anthropic.com"
|
||||
or _hostname.endswith(".anthropic.com")
|
||||
):
|
||||
return JSONResponse({"error": "api_key is required"}, status_code=400)
|
||||
|
||||
|
||||
@@ -720,7 +720,9 @@ function buildNodeRow(node) {
|
||||
function toggleGroup(prefix) {
|
||||
expandedGroups[prefix] = !expandedGroups[prefix];
|
||||
var body = document.querySelector(
|
||||
'.node-group-body[data-prefix="' + prefix.replace(/"/g, '\\"') + '"]',
|
||||
'.node-group-body[data-prefix="' +
|
||||
prefix.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
|
||||
'"]',
|
||||
);
|
||||
if (!body) return;
|
||||
var isExpanded = expandedGroups[prefix];
|
||||
|
||||
@@ -546,7 +546,11 @@ def _detect_openai_compat(
|
||||
result["context_window"] = known["context_window"]
|
||||
|
||||
# Server type heuristics
|
||||
if base_url and "api.openai.com" in base_url:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
_normalized = (base_url if "://" in base_url else f"https://{base_url}") if base_url else ""
|
||||
_hostname = urlparse(_normalized).hostname or "" if _normalized else ""
|
||||
if base_url and (_hostname == "api.openai.com" or _hostname.endswith(".openai.com")):
|
||||
result["server_type"] = "openai"
|
||||
elif meta is not None and "n_ctx_train" in meta:
|
||||
result["server_type"] = "llama.cpp"
|
||||
|
||||
+5
-1
@@ -45,7 +45,11 @@ _MCP_ONLY_TOOLS = frozenset({"read_resource", "use_prompt"})
|
||||
|
||||
def _detect_provider(base_url: str) -> str:
|
||||
"""Infer provider name from a base URL."""
|
||||
if "anthropic.com" in base_url:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
normalized = base_url if "://" in base_url else f"https://{base_url}"
|
||||
hostname = urlparse(normalized).hostname or ""
|
||||
if hostname == "anthropic.com" or hostname.endswith(".anthropic.com"):
|
||||
return "anthropic"
|
||||
return "openai"
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ function inlineMarkdown(text) {
|
||||
text = text.replace(/~~(.+?)~~/g, "<del>$1</del>");
|
||||
// Images (must come before links — render as click-to-load placeholder)
|
||||
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (m, alt, url) {
|
||||
if (!/^\s*(https?:\/\/|data:image\/)/i.test(url)) return m;
|
||||
var safeAlt = alt || "Image";
|
||||
var domain = "";
|
||||
try {
|
||||
@@ -53,9 +54,9 @@ function inlineMarkdown(text) {
|
||||
"</span>"
|
||||
);
|
||||
});
|
||||
// Links (block javascript: scheme)
|
||||
// Links (allow http, https, and same-origin relative URLs only)
|
||||
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (m, label, url) {
|
||||
if (/^\s*javascript:/i.test(url)) return m;
|
||||
if (!/^\s*(https?:\/\/|\/(?!\/))/i.test(url)) return m;
|
||||
return (
|
||||
'<a href="' +
|
||||
url +
|
||||
@@ -89,8 +90,16 @@ function inlineMarkdown(text) {
|
||||
document.addEventListener("click", function (e) {
|
||||
var ph = e.target.closest(".img-placeholder");
|
||||
if (!ph) return;
|
||||
var raw = ph.getAttribute("data-src") || "";
|
||||
if (!/^(https?:\/\/|data:image\/)/i.test(raw)) return;
|
||||
var src;
|
||||
try {
|
||||
src = new URL(raw).href;
|
||||
} catch (_e) {
|
||||
return;
|
||||
}
|
||||
var img = document.createElement("img");
|
||||
img.src = ph.getAttribute("data-src");
|
||||
img.src = src;
|
||||
img.alt = ph.getAttribute("data-alt");
|
||||
img.loading = "lazy";
|
||||
ph.replaceWith(img);
|
||||
|
||||
Reference in New Issue
Block a user