T09 · Insecure Skill Coding Practices
- Location
scripts/jevw-escaler.py:52- Finding
Untrusted webpage content can steer actions in an authenticated browser session
- Content
View full analysis
Vulnerability Details
File Location:
src/jev_webbridge/table.py:46-69,scripts/jevw-escaler.py:52-84,src/jev_webbridge/loop.py:330-369,src/jev_webbridge/executor.py:139-180
Vulnerability Type: Indirect prompt injection into an autonomous browser-action decision
Risk Level: MediumComplete Code Snippets
src/jev_webbridge/table.py:46-69collects attacker-controlled webpage labels and values:javascript const els = Array.from(document.querySelectorAll(sel)) .filter(el => VIS(el) && !el.disabled) .filter(el => !(el.closest('[aria-hidden="true"]'))); const rows = []; const rowEls = []; els.forEach((el, i) => { const ops = OPS(el); let name = (el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80); if (!name && el.id) { const lb = document.querySelector('label[for="' + el.id.replace(/"/g, '') + '"]'); if (lb) name = (lb.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80); } let value = null; if ('value' in el && typeof el.value === 'string') value = el.value.slice(0, 80); else if (el.isContentEditable) value = (el.textContent || '').slice(0, 80); const typeable = ops.includes('TYPE_TEXT'); if (!name && !value && !typeable) return; if (!name) name = '(unnamed ' + (el.getAttribute('role') || el.tagName.toLowerCase()) + ')'; el.setAttribute('data-jw-idx', String(rows.length)); rowEls.push(el); rows.push({idx: rows.length, role: (el.getAttribute('role') || el.tagName.toLowerCase()), name, value, ops}); });scripts/jevw-escaler.py:52-84places those labels directly into an LLM prompt and converts its response into an executable decision:python rows = payload.get("rows") or [] table = "\n".join( f"{r.get('idx')} | {r.get('role')} | {(r.get('name') or r.get('value') or '')[:60] ...[truncated 7282 chars]- Remediation
View remediation
Remediation Suggestions
- Treat every page-derived field as untrusted data. Serialize element metadata in a clearly delimited structured object and explicitly instruct the decision model that text inside those fields is data, not executable instructions.
- Add a deterministic post-escalation validator before
executor.act. At minimum, verify that the target exists in the observed rows, that the operation is supported by that row, and that the operation is permitted for the current workflow state. - Add an authorization policy for consequential actions. Require explicit user confirmation before activating controls associated with submission, deletion, payment, publication, permission changes, account changes, or cross-origin navigation.
- Bind each run to an allowlisted origin or set of origins. Pause for confirmation when an action would leave the user-approved origin.
- Do not default an LLM-selected
TYPE_TEXTaction to the complete goal text unless the target has been deterministically identified as the intended query field. Require an approved field classification or explicit target binding. - Prefer deterministic site adapters for high-impact workflows rather than allowing a generic LLM to choose among every visible interactive control.
- Preserve receipts, but expose the proposed action to the user before execution when a confirmation policy is triggered. Logging after a decision must not be treated as authorization.
- Add adversarial tests with instruction-like button labels, placeholders, links, and accessibility labels to verify that page text cannot override goal and origin restrictions.
