diff --git a/api/routes.py b/api/routes.py index 9f9d276..0ddf44d 100644 --- a/api/routes.py +++ b/api/routes.py @@ -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 diff --git a/static/index.html b/static/index.html index ecc6653..544796f 100644 --- a/static/index.html +++ b/static/index.html @@ -6,14 +6,14 @@ Hermes - - - + + +
-
Hermes
Start a new conversation
+
Hermes
Start a new conversation
GPT-5.4 Mini
diff --git a/static/ui.js b/static/ui.js index 679c89e..0d1ca38 100644 --- a/static/ui.js +++ b/static/ui.js @@ -88,12 +88,12 @@ function renderMd(raw){ }); s=s.replace(/```([\w+-]*)\n?([\s\S]*?)```/g,(_,lang,code)=>{const h=lang?`
${esc(lang)}
`:'';return `${h}
${esc(code.replace(/\n$/,''))}
`;}); s=s.replace(/`([^`\n]+)`/g,(_,c)=>`${esc(c)}`); - s=s.replace(/\*\*\*(.+?)\*\*\*/g,'$1'); - s=s.replace(/\*\*(.+?)\*\*/g,'$1'); - s=s.replace(/\*([^*\n]+)\*/g,'$1'); - s=s.replace(/^### (.+)$/gm,'

$1

').replace(/^## (.+)$/gm,'

$1

').replace(/^# (.+)$/gm,'

$1

'); + s=s.replace(/\*\*\*(.+?)\*\*\*/g,(_,t)=>`${esc(t)}`); + s=s.replace(/\*\*(.+?)\*\*/g,(_,t)=>`${esc(t)}`); + s=s.replace(/\*([^*\n]+)\*/g,(_,t)=>`${esc(t)}`); + s=s.replace(/^### (.+)$/gm,(_,t)=>`

${esc(t)}

`).replace(/^## (.+)$/gm,(_,t)=>`

${esc(t)}

`).replace(/^# (.+)$/gm,(_,t)=>`

${esc(t)}

`); s=s.replace(/^---+$/gm,'
'); - s=s.replace(/^> (.+)$/gm,'
$1
'); + s=s.replace(/^> (.+)$/gm,(_,t)=>`
${esc(t)}
`); // 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+=`
  • ${text}
  • `; - else html+=`
  • ${text}
  • `; + if(indent) html+=`
  • ${esc(text)}
  • `; + else html+=`
  • ${esc(text)}
  • `; } return html+''; }); @@ -111,19 +111,19 @@ function renderMd(raw){ let html='
      '; for(const l of lines){ const text=l.replace(/^ {0,4}\d+\. /,''); - html+=`
    1. ${text}
    2. `; + html+=`
    3. ${esc(text)}
    4. `; } return html+'
    '; }); - s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,'$1'); + s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,label,url)=>`${esc(label)}`); // 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=>`${c.trim()}`).join(''); - const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`${c.trim()}`).join(''); + const parseRow=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`${esc(c.trim())}`).join(''); + const parseHeader=r=>r.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(c=>`${esc(c.trim())}`).join(''); const header=`${parseHeader(rows[0])}`; const body=rows.slice(2).map(r=>`${parseRow(r)}`).join(''); return `${header}${body}
    `; @@ -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:{