release: v1.6.1 link sanitization and backlog updates
This commit is contained in:
23
app.py
23
app.py
@@ -164,6 +164,17 @@ def format_direct_answer(question: str, results: list[dict]) -> str:
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def sanitize_outbound_url(url: str) -> str:
|
||||
"""Allow only absolute http/https URLs for outbound links shown in UI."""
|
||||
if not url:
|
||||
return ""
|
||||
candidate = url.strip()
|
||||
parsed = urlparse(candidate)
|
||||
if parsed.scheme.lower() in {"http", "https"} and parsed.netloc:
|
||||
return candidate
|
||||
return ""
|
||||
|
||||
|
||||
# --- Default Profile ---
|
||||
DEFAULT_PROFILE = """You are a coding companion running locally on a machine called "jarvis".
|
||||
|
||||
@@ -571,7 +582,7 @@ async def query_searxng(query: str, max_results: int = 5) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"title": "Current Weather",
|
||||
"url": f"https://wttr.in/{location}",
|
||||
"url": sanitize_outbound_url(f"https://wttr.in/{location}"),
|
||||
"content": resp.text.strip(),
|
||||
}
|
||||
]
|
||||
@@ -603,9 +614,11 @@ async def query_searxng(query: str, max_results: int = 5) -> list[dict]:
|
||||
results.append(
|
||||
{
|
||||
"title": box.get("infobox", "Info"),
|
||||
"url": box.get("urls", [{}])[0].get("url", "")
|
||||
if box.get("urls")
|
||||
else "",
|
||||
"url": sanitize_outbound_url(
|
||||
box.get("urls", [{}])[0].get("url", "")
|
||||
if box.get("urls")
|
||||
else ""
|
||||
),
|
||||
"content": content,
|
||||
}
|
||||
)
|
||||
@@ -613,7 +626,7 @@ async def query_searxng(query: str, max_results: int = 5) -> list[dict]:
|
||||
results.append(
|
||||
{
|
||||
"title": r.get("title", ""),
|
||||
"url": r.get("url", ""),
|
||||
"url": sanitize_outbound_url(r.get("url", "")),
|
||||
"content": r.get("content", ""),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ Total identified items: 26
|
||||
- P3: Nice-to-have polish.
|
||||
|
||||
## Top 10 (Urgency Order)
|
||||
1. [P0] Add authentication/authorization for all write and admin endpoints.
|
||||
2. [P0] Add CSRF/origin protection for browser-initiated state-changing requests.
|
||||
3. [P0] Block unsafe URL schemes in rendered search-result links (e.g., javascript:).
|
||||
1. [P0][DONE] Add authentication/authorization for all write and admin endpoints.
|
||||
2. [P0][DONE] Add CSRF/origin protection for browser-initiated state-changing requests.
|
||||
3. [P0][DONE] Block unsafe URL schemes in rendered search-result links (e.g., javascript:).
|
||||
4. [P0] Add rate limiting and request body size limits for chat/search/profile APIs.
|
||||
5. [P1] Restrict settings updates to an allowlist of valid keys.
|
||||
6. [P1] Add pagination + hard caps on list endpoints (memories, conversations, message history).
|
||||
|
||||
@@ -44,9 +44,9 @@ Total identified items: 26
|
||||
|
||||
Top 10 (brief):
|
||||
|
||||
1. P0: Add auth for write/admin endpoints
|
||||
2. P0: Add CSRF/origin protection for state-changing requests
|
||||
3. P0: Block unsafe URL schemes in rendered links
|
||||
1. P0 [DONE]: Add auth for write/admin endpoints
|
||||
2. P0 [DONE]: Add CSRF/origin protection for state-changing requests
|
||||
3. P0 [DONE]: Block unsafe URL schemes in rendered links
|
||||
4. P0: Add rate limiting and request size limits
|
||||
5. P1: Restrict `/api/settings` updates to allowlisted keys
|
||||
6. P1: Add pagination + hard caps for list APIs
|
||||
@@ -57,7 +57,7 @@ Top 10 (brief):
|
||||
|
||||
Item 1 executive summary: keep guest mode for conversational chat, require 4-digit admin PIN for advanced/destructive actions, and enforce local/LAN-only backend policy by default.
|
||||
|
||||
Implementation status: complete (guest session by default + admin unlock + admin-only write enforcement + origin checks + audit logging + capability tests).
|
||||
Implementation status: complete (guest session by default + admin unlock + admin-only write enforcement + origin checks + safe-link sanitization + audit logging + capability tests).
|
||||
|
||||
## TODO
|
||||
|
||||
|
||||
@@ -970,7 +970,13 @@ async function sendSearch() {
|
||||
if (data.raw_results) {
|
||||
let rawHtml = '<details class="raw-results"><summary>🔍 View raw search results (' + data.raw_results.length + ')</summary><ul>';
|
||||
data.raw_results.forEach(r => {
|
||||
rawHtml += `<li><a href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>`;
|
||||
const safeUrl = sanitizeUrl(r.url || '');
|
||||
rawHtml += '<li>';
|
||||
if (safeUrl) {
|
||||
rawHtml += `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>`;
|
||||
} else {
|
||||
rawHtml += `<span>${escapeHtml(r.title)}</span>`;
|
||||
}
|
||||
if (r.content) rawHtml += `<small>${escapeHtml(r.content)}</small>`;
|
||||
rawHtml += '</li>';
|
||||
});
|
||||
@@ -1102,6 +1108,17 @@ function renderMarkdown(text) {
|
||||
return h;
|
||||
}
|
||||
|
||||
function sanitizeUrl(url) {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.href;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function addCopyButtons(msgDiv) {
|
||||
msgDiv.querySelectorAll('pre').forEach(pre => {
|
||||
if (pre.querySelector('.copy-btn')) return;
|
||||
|
||||
17
tests/test_search_url_sanitization.py
Normal file
17
tests/test_search_url_sanitization.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import app as app_module
|
||||
|
||||
|
||||
def test_sanitize_outbound_url_allows_http_https():
|
||||
assert app_module.sanitize_outbound_url("https://example.com/path") == "https://example.com/path"
|
||||
assert app_module.sanitize_outbound_url("http://example.com") == "http://example.com"
|
||||
|
||||
|
||||
def test_sanitize_outbound_url_blocks_unsafe_schemes():
|
||||
assert app_module.sanitize_outbound_url("javascript:alert(1)") == ""
|
||||
assert app_module.sanitize_outbound_url("data:text/html,evil") == ""
|
||||
assert app_module.sanitize_outbound_url("file:///etc/passwd") == ""
|
||||
|
||||
|
||||
def test_sanitize_outbound_url_blocks_relative_and_empty():
|
||||
assert app_module.sanitize_outbound_url("/relative/path") == ""
|
||||
assert app_module.sanitize_outbound_url("") == ""
|
||||
Reference in New Issue
Block a user