Merge pull request #13 from nesquena/fix/security-hardening
fix(security): 5 security hardening fixes
This commit is contained in:
@@ -129,6 +129,9 @@ def handle_get(handler, parsed):
|
||||
return _handle_approval_pending(handler, parsed)
|
||||
|
||||
if parsed.path == '/api/approval/inject_test':
|
||||
# Loopback-only: used by automated tests; blocked from any remote client
|
||||
if handler.client_address[0] != '127.0.0.1':
|
||||
return j(handler, {'error': 'not found'}, status=404)
|
||||
return _handle_approval_inject(handler, parsed)
|
||||
|
||||
# ── Cron API (GET) ──
|
||||
@@ -335,10 +338,9 @@ def handle_post(handler, parsed):
|
||||
|
||||
def _serve_static(handler, parsed):
|
||||
static_root = (Path(__file__).parent.parent / 'static').resolve()
|
||||
# Strip the leading '/static/' prefix and resolve the full path
|
||||
# Strip the leading '/static/' prefix, then resolve and sandbox
|
||||
rel = parsed.path[len('/static/'):]
|
||||
static_file = (static_root / rel).resolve()
|
||||
# Sandbox check: resolved path must stay inside static_root
|
||||
try:
|
||||
static_file.relative_to(static_root)
|
||||
except ValueError:
|
||||
@@ -494,6 +496,7 @@ def _handle_approval_pending(handler, parsed):
|
||||
|
||||
|
||||
def _handle_approval_inject(handler, parsed):
|
||||
"""Inject a fake pending approval -- loopback-only, used by automated tests."""
|
||||
qs = parse_qs(parsed.query)
|
||||
sid = qs.get('session_id', [''])[0]
|
||||
key = qs.get('pattern_key', ['test_pattern'])[0]
|
||||
@@ -879,6 +882,8 @@ def _handle_skill_save(handler, body):
|
||||
if not skill_name or '/' in skill_name or '..' in skill_name:
|
||||
return bad(handler, 'Invalid skill name')
|
||||
category = body.get('category', '').strip()
|
||||
if category and ('/' in category or '..' in category):
|
||||
return bad(handler, 'Invalid category')
|
||||
from tools.skills_tool import SKILLS_DIR
|
||||
if category:
|
||||
skill_dir = SKILLS_DIR / category / skill_name
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<title>Hermes</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<!-- Prism.js syntax highlighting (loaded async, non-blocking) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js" defer></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" integrity="sha384-wFjoQjtV1y5jVHbt0p35Ui8aV8GVpEZkyF99OXWqP/eNJDU93D3Ugxkoyh6Y2I4A" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js" integrity="sha384-MXybTpajaBV0AkcBaCPT4KIvo0FzoCiWXgcihYsw4FUkEz0Pv3JGV6tk2G8vJtDc" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js" integrity="sha384-Uq05+JLko69eOiPr39ta9bh7kld5PKZoU+fF7g0EXTAriEollhZ+DrN8Q/Oi8J2Q" crossorigin="anonymous" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.2</div></div></div>
|
||||
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.16</div></div></div>
|
||||
<div class="sidebar-nav">
|
||||
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">💬</button>
|
||||
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">📅</button>
|
||||
@@ -143,7 +143,7 @@
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<div class="topbar-left" style="flex:1;min-width:0;overflow:hidden"><div class="topbar-title" id="topbarTitle">Hermes</div><div class="topbar-meta" id="topbarMeta">Start a new conversation</div></div>
|
||||
<div style="flex:1;min-width:0;overflow:hidden"><div class="topbar-title" id="topbarTitle">Hermes</div><div class="topbar-meta" id="topbarMeta">Start a new conversation</div></div>
|
||||
<div class="topbar-chips">
|
||||
<div class="chip model" id="modelChip">GPT-5.4 Mini</div>
|
||||
<div id="wsChipWrap" style="position:relative">
|
||||
|
||||
26
static/ui.js
26
static/ui.js
@@ -88,12 +88,12 @@ function renderMd(raw){
|
||||
});
|
||||
s=s.replace(/```([\w+-]*)\n?([\s\S]*?)```/g,(_,lang,code)=>{const h=lang?`<div class="pre-header">${esc(lang)}</div>`:'';return `${h}<pre><code>${esc(code.replace(/\n$/,''))}</code></pre>`;});
|
||||
s=s.replace(/`([^`\n]+)`/g,(_,c)=>`<code>${esc(c)}</code>`);
|
||||
s=s.replace(/\*\*\*(.+?)\*\*\*/g,'<strong><em>$1</em></strong>');
|
||||
s=s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
|
||||
s=s.replace(/\*([^*\n]+)\*/g,'<em>$1</em>');
|
||||
s=s.replace(/^### (.+)$/gm,'<h3>$1</h3>').replace(/^## (.+)$/gm,'<h2>$1</h2>').replace(/^# (.+)$/gm,'<h1>$1</h1>');
|
||||
s=s.replace(/\*\*\*(.+?)\*\*\*/g,(_,t)=>`<strong><em>${esc(t)}</em></strong>`);
|
||||
s=s.replace(/\*\*(.+?)\*\*/g,(_,t)=>`<strong>${esc(t)}</strong>`);
|
||||
s=s.replace(/\*([^*\n]+)\*/g,(_,t)=>`<em>${esc(t)}</em>`);
|
||||
s=s.replace(/^### (.+)$/gm,(_,t)=>`<h3>${esc(t)}</h3>`).replace(/^## (.+)$/gm,(_,t)=>`<h2>${esc(t)}</h2>`).replace(/^# (.+)$/gm,(_,t)=>`<h1>${esc(t)}</h1>`);
|
||||
s=s.replace(/^---+$/gm,'<hr>');
|
||||
s=s.replace(/^> (.+)$/gm,'<blockquote>$1</blockquote>');
|
||||
s=s.replace(/^> (.+)$/gm,(_,t)=>`<blockquote>${esc(t)}</blockquote>`);
|
||||
// B8: improved list handling supporting up to 2 levels of indentation
|
||||
s=s.replace(/((?:^(?: )?[-*+] .+\n?)+)/gm,block=>{
|
||||
const lines=block.trimEnd().split('\n');
|
||||
@@ -101,8 +101,8 @@ function renderMd(raw){
|
||||
for(const l of lines){
|
||||
const indent=/^ {2,}/.test(l);
|
||||
const text=l.replace(/^ {0,4}[-*+] /,'');
|
||||
if(indent) html+=`<li style="margin-left:16px">${text}</li>`;
|
||||
else html+=`<li>${text}</li>`;
|
||||
if(indent) html+=`<li style="margin-left:16px">${esc(text)}</li>`;
|
||||
else html+=`<li>${esc(text)}</li>`;
|
||||
}
|
||||
return html+'</ul>';
|
||||
});
|
||||
@@ -111,19 +111,19 @@ function renderMd(raw){
|
||||
let html='<ol>';
|
||||
for(const l of lines){
|
||||
const text=l.replace(/^ {0,4}\d+\. /,'');
|
||||
html+=`<li>${text}</li>`;
|
||||
html+=`<li>${esc(text)}</li>`;
|
||||
}
|
||||
return html+'</ol>';
|
||||
});
|
||||
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,label,url)=>`<a href="${esc(url)}" target="_blank" rel="noopener">${esc(label)}</a>`);
|
||||
// Tables: | col | col | header row followed by | --- | --- | separator then data rows
|
||||
s=s.replace(/((?:^\|.+\|\n?)+)/gm,block=>{
|
||||
const rows=block.trim().split('\n').filter(r=>r.trim());
|
||||
if(rows.length<2)return block;
|
||||
const isSep=r=>/^\|[\s|:-]+\|$/.test(r.trim());
|
||||
if(!isSep(rows[1]))return block;
|
||||
const parseRow=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<td>${c.trim()}</td>`).join('');
|
||||
const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<th>${c.trim()}</th>`).join('');
|
||||
const parseRow=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<td>${esc(c.trim())}</td>`).join('');
|
||||
const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`<th>${esc(c.trim())}</th>`).join('');
|
||||
const header=`<tr>${parseHeader(rows[0])}</tr>`;
|
||||
const body=rows.slice(2).map(r=>`<tr>${parseRow(r)}</tr>`).join('');
|
||||
return `<table><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
@@ -568,7 +568,9 @@ function renderMermaidBlocks(){
|
||||
if(!_mermaidLoading){
|
||||
_mermaidLoading=true;
|
||||
const script=document.createElement('script');
|
||||
script.src='https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
|
||||
script.src='https://cdn.jsdelivr.net/npm/mermaid@10.9.3/dist/mermaid.min.js';
|
||||
script.integrity='sha384-R63zfMfSwJF4xCR11wXii+QUsbiBIdiDzDbtxia72oGWfkT7WHJfmD/I/eeHPJyT';
|
||||
script.crossOrigin='anonymous';
|
||||
script.onload=()=>{
|
||||
if(typeof mermaid!=='undefined'){
|
||||
mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{
|
||||
|
||||
Reference in New Issue
Block a user