Back to skill

Security audit

Omni Research

Security checks for vulnerabilities and agentic risk

Overview

This research skill is not proven malicious, but it needs Review because it controls logged-in browser sessions and can send collected content to a configurable API/proxy despite browser-only and zero-key framing.

Install only if you are comfortable letting the skill operate a logged-in Chromium browser through CDP. Use a dedicated browser profile, avoid sensitive account data or confidential research, review ~/.config/omni-research/config.json before use, and treat API mode or synthesis as sending your query and extracted results to the configured proxy endpoint.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (5)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
research.py:54
Finding
Overly Broad DOM Extraction May Capture Unrelated Authenticated Browser Content<![CDATA[ ## Vulnerability Details **File Location**: `research.py:54-62`, `research.py:104-112`, `research.py:142-151` **Vulnerability Type**: Excessive access to authenticated browser-page content **Risk Level**: High ### Vulnerable Code ```python # research.py:54-62 const bodyText = document.body.innerText; // Check if still loading if (bodyText.includes('思考中') || bodyText.includes('搜尋中') || bodyText.includes('Searching')) return ''; // Strategy 1: find .prose or markdown containers const prose = document.querySelectorAll('.prose, [class*="prose"], [class*="markdown"]'); if (prose.length > 0) { const last = prose[prose.length-1].innerText; if (last.length > 100) return last; } // Strategy 2: find answer text from body content const lines = bodyText.split('\n').filter(l => l.trim().length > 40); if (lines.length > 3) return lines.join('\n'); ``` ```python # research.py:104-112 const scroll = document.querySelector('[class*="overflow-y-auto"][class*="scrollbar-gutter"]'); if (scroll && scroll.innerText.length > 50) { // Strip the user query (first line) to get just the response const lines = scroll.innerText.split('\n').filter(l => l.trim()); const responseStart = lines.findIndex((l, i) => i > 0 && l.length > 20); if (responseStart > 0) return lines.slice(responseStart).join('\n'); } return ''; ``` ```python # research.py:142-151 const allDivs = document.querySelectorAll('div'); let best = null, bestLen = 0; for (const d of allDivs) { const t = d.innerText; if (t.length > 200 && t.length < 15000 && !d.querySelector('nav, header, aside')) { if (t.length > bestLen) { bestLen = t.length; best = d; } } } if (best) return best.innerText; return ''; ``` ### Technical Analysis The Skill runs JavaScript inside pages opened under the user's authenticated Perplexity, Grok, and Gemini browser sessions. Its fallback extractors are not reliably limited to the response generated for the current query. The Perplexity fallback ...[truncated 1767 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove all whole-document, generic scroll-container, and largest-element fallbacks. 2. Restrict extraction to a response element associated with the current request. 3. Record page state before submission and extract only response nodes created afterward. 4. Validate the active origin before evaluating extraction JavaScript. 5. Prefer stable service-specific identifiers, accessibility roles, and message ownership attributes. 6. Fail closed with an extraction error when the current response cannot be identified precisely. 7. Strip navigation, historical messages, user messages, hidden elements, and account-interface content. 8. Add automated tests proving that previous chats and sidebar content cannot enter extracted results. 9. Require explicit user confirmation before sending browser-derived content to any synthesis service. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
research.py:165
Finding
Sensitive Queries and Browser-Derived Content Can Be Sent to an Arbitrary Configured Proxy<![CDATA[ ## Vulnerability Details **File Location**: `research.py:165-174`, `research.py:198-210`, `research.py:249-253` **Vulnerability Type**: Unrestricted outbound destination and insecure plaintext transmission **Risk Level**: High ### Vulnerable Code ```python # research.py:165-174 async def _cliproxy_chat(config: dict, model: str, messages: list[dict]) -> str: url = config["cliproxy_url"] key = config["cliproxy_key"] async with httpx.AsyncClient(timeout=config["timeout_api"]) as client: for attempt in range(3): resp = await client.post( f"{url}/chat/completions", headers={"Authorization": f"Bearer {key}"}, json={"model": model, "messages": messages, "max_tokens": 4096}, ) ``` ```python # research.py:198-210 async def synthesize(config: dict, query: str, results: dict[str, str]) -> str: sources_text = "" for name, text in results.items(): truncated = text[:3000] if len(text) > 3000 else text sources_text += f"\n\n### {name}\n{truncated}" # Try synthesis model, fall back to gemini if rate limited for model in [config["synthesis_model"], config["gemini_api_model"]]: try: return await _cliproxy_chat( config, model, [ {"role": "system", "content": "Synthesize these research results into 3-5 bullet points. Note agreements, disagreements, unique insights. Write in the query's language."}, {"role": "user", "content": f"Query: {query}\n\nSources:{sources_text}"}, ], ) ``` ```python # research.py:249-253 except RuntimeError as e: print(f" {e}", file=sys.stderr) sources = [s for s in sources if s in API_SOURCES] or ["gemini-api"] print(f" Falling back to: {', '.join(sources)}", file=sys.stderr) ``` ### Technical Analysis The proxy URL is loaded from a user-writable configuration file and used directly as th ...[truncated 1919 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict the default endpoint to an explicitly verified loopback address. 2. Reject non-loopback destinations unless the user enables remote transmission through a separate, explicit setting. 3. Require HTTPS for every non-loopback endpoint. 4. Maintain an allowlist of approved hosts and ports rather than accepting an arbitrary URL. 5. Resolve hostnames and defend against redirects or DNS rebinding to unapproved destinations. 6. Disable HTTP redirects or revalidate every redirect target against the allowlist. 7. Display the destination and categories of data to be transmitted before the first request. 8. Remove automatic API fallback or require explicit user confirmation before changing processing modes. 9. Minimize transmitted content and redact account identifiers, historical messages, credentials, and other sensitive text. 10. Separate browser extraction from synthesis so users can inspect and approve source content before transmission. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
research.py:20
Finding
Hardcoded Proxy Credential Is Used When No Configuration File Exists<![CDATA[ ## Vulnerability Details **File Location**: `research.py:20-31`, `setup.py:17-24` **Vulnerability Type**: Hardcoded credential and inconsistent secret configuration **Risk Level**: Medium ### Vulnerable Code ```python # research.py:20-31 CONFIG_PATH = Path.home() / ".config" / "omni-research" / "config.json" DEFAULT_CONFIG = { "cdp_port": None, "cliproxy_url": "http://127.0.0.1:8317/v1", "cliproxy_key": "magi-proxy-key-2026", "synthesis_model": "glm-4.7", "gemini_api_model": "gemini-2.5-flash", "timeout_browser": 180, "timeout_api": 30, } ``` ```python # setup.py:17-24 DEFAULT_CONFIG = { "cdp_port": 9222, "cliproxy_url": "http://127.0.0.1:8317/v1", "cliproxy_key": "", "synthesis_model": "glm-4.7", "gemini_api_model": "gemini-2.5-flash", } ``` ### Technical Analysis `research.py` embeds a static bearer credential directly in source code. If the configuration file does not exist or does not override the key, this value is automatically added to proxy requests. The setup utility creates a different default configuration with an empty key. Consequently, authentication behavior changes depending on whether setup was previously run. A repository-visible shared credential cannot provide meaningful secrecy, per-installation identity, or safe rotation. The issue is particularly relevant to local multi-user systems or environments where a compatible proxy accepts this static value. A malicious local service listening on the default port would also receive the credential and request content. ### Attack Path 1. The user runs `research.py` without first creating a configuration file. 2. Runtime defaults select the hardcoded bearer value. 3. A local proxy, malicious process, or service bound to `127.0.0.1:8317` receives the request. 4. The static credential and research content are exposed to that service. 5. If a real proxy deployment accepts the shared key, another party who learned it from the public sourc ...[truncated 566 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `magi-proxy-key-2026` from the runtime defaults. 2. Refuse authenticated API operation until the user explicitly supplies a credential. 3. Load secrets from an environment variable, operating-system keychain, or dedicated secret manager. 4. Keep non-secret endpoint settings separate from secret material. 5. Make setup and runtime defaults consistent. 6. If a configuration file stores a secret, create it with owner-only permissions such as mode `0600`. 7. Support credential rotation and use unique credentials per installation. 8. Avoid sending an `Authorization` header when no credential is configured. 9. Rotate or revoke the disclosed static value wherever it has been accepted. ]]>

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding
Unpinned Dependencies and Unnecessary Global Package Installation Expand Supply-Chain Risk<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:1-2`, `setup.py:47-51` **Vulnerability Type**: Mutable dependency resolution and unpinned global installation guidance **Risk Level**: Medium ### Vulnerable Code ```text # requirements.txt:1-2 httpx>=0.27 websockets>=12.0 ``` ```python # setup.py:47-51 if check_agent_browser(): print("[OK] agent-browser installed") else: print("[!!] agent-browser not found") print(" Install: npm install -g agent-browser") ``` ### Technical Analysis The Python dependencies specify only minimum versions. Installation can therefore resolve to any future version, including releases that were not reviewed with this Skill. No lockfile or integrity hashes are provided. The setup utility also recommends a global installation of the unpinned npm package `agent-browser`. The reviewed browser implementation uses `httpx`, `websockets`, and direct CDP calls; it does not invoke `agent-browser`. This makes the suggested global package an unnecessary expansion of the trusted computing base. Package installation commonly executes package-controlled build or lifecycle code with the installing user's privileges. Mutable version resolution weakens reproducibility and can expose users to compromised future releases or unexpected behavior changes. ### Attack Path 1. A user installs dependencies from `requirements.txt` or follows the setup instruction. 2. The package manager resolves the newest versions permitted by the open-ended constraints. 3. A future compromised, malicious, or incompatible package release is selected. 4. Package installation hooks or imported runtime code execute under the user's account. 5. For the globally installed npm package, the component becomes available system-wide for that user despite not being required by the reviewed implementation. ### Impact Assessment The exact impact depends on the behavior of a future selected package release; no currently malicious dependency was ...[truncated 363 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin dependencies to reviewed exact versions. 2. Generate a lockfile containing transitive dependency versions. 3. Use package hashes, such as pip hash-checking mode, to verify artifact integrity. 4. Review and update dependencies through a controlled process with vulnerability scanning. 5. Install dependencies in an isolated virtual environment rather than globally. 6. Remove the `agent-browser` installation recommendation unless the package becomes a necessary, reviewed dependency. 7. If it is required later, pin its exact version and document its package source and integrity verification. 8. Add automated dependency auditing and reproducible installation tests to release procedures. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
research.py:198
Finding
Untrusted Source Responses Can Inject Instructions into the Synthesis Prompt<![CDATA[ ## Vulnerability Details **File Location**: `research.py:198-210` **Vulnerability Type**: Indirect prompt injection through untrusted model output **Risk Level**: Medium ### Vulnerable Code ```python # research.py:198-210 async def synthesize(config: dict, query: str, results: dict[str, str]) -> str: sources_text = "" for name, text in results.items(): truncated = text[:3000] if len(text) > 3000 else text sources_text += f"\n\n### {name}\n{truncated}" # Try synthesis model, fall back to gemini if rate limited for model in [config["synthesis_model"], config["gemini_api_model"]]: try: return await _cliproxy_chat( config, model, [ {"role": "system", "content": "Synthesize these research results into 3-5 bullet points. Note agreements, disagreements, unique insights. Write in the query's language."}, {"role": "user", "content": f"Query: {query}\n\nSources:{sources_text}"}, ], ) ``` ### Technical Analysis Responses from external AI services are untrusted content. The implementation concatenates them directly into a synthesis prompt without strong delimiters, structured fields, provenance controls, or a system-level instruction that source-contained commands must not be followed. A malicious or compromised source can return text such as instructions to ignore other sources, conceal disagreements, add attacker-selected statements, or alter the required output. The synthesis model may interpret those instructions as part of the active task rather than as quoted research material. The reviewed synthesis model has no local tool access through this code, so the confirmed impact is output manipulation rather than direct system compromise. ### Attack Path 1. A queried external service returns adversarial text, or its source material causes it to reproduce prompt-injection instructions. 2. The text is stored ...[truncated 821 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. State in the system prompt that all source content is untrusted data and that instructions inside it must never be followed. 2. Pass sources through a structured format with separate source name, content, and trust metadata fields. 3. Use explicit delimiters and describe their contents as quotations rather than instructions. 4. Normalize or flag phrases that attempt to override instructions, request secret disclosure, or control output format. 5. Require the synthesis to cite which source supports each claim. 6. Preserve disagreements rather than allowing one source to override all others. 7. Add tests containing adversarial source responses and verify that injected instructions are ignored. 8. Where integrity is critical, perform deterministic preprocessing and validation before asking a model to synthesize results. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documented purpose emphasizes using the user's own browser sessions with 'zero API keys,' but the behavior includes external API usage, local config reads, and synthesis through a remote proxy/model endpoint. This mismatch is dangerous because sensitive prompts, extracted browser-session data, or research results may be sent off-device or accessed through undeclared channels, defeating user expectations and informed consent.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill declares powerful capabilities in practice—network access, shell execution, and file access patterns—without an explicit permission or allowed-tools scope. That makes the trust boundary unclear and can cause a host agent to grant broader access than a user would reasonably expect from the manifest alone.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The skill advertises 'No API keys' and reliance on existing subscriptions, yet also supports a key-based API mode. While not inherently malicious, this is a security-significant disclosure failure because users may invoke the skill under incorrect assumptions about where credentials are required and how data is routed.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
Connecting to a browser's remote debugging interface gives the skill the ability to open tabs, inject input, read page contents, and act inside already-authenticated sessions. Failing to prominently warn users about this is dangerous because CDP access is highly privileged and can expose account data or perform actions as the user across multiple services.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The eval_js helper executes arbitrary JavaScript in the context of the currently loaded page and returns the extracted value, enabling silent reading of page content. In a live user browser, this can expose sensitive data from authenticated pages, including prompts, search results, account information, or other DOM-accessible content, without visible disclosure to the user.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The code opens browser tabs, brings them to the foreground, injects text, presses Enter, and closes tabs without any explicit user consent or runtime confirmation in this module. Because it connects to the user's live browser session via CDP, those actions execute with the user's authenticated state and can trigger searches, form submissions, or other side effects on real sites.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The skill description promises 'zero API keys' and use of the user's existing subscriptions, but the code embeds and uses a separate bearer token and proxy endpoint. This is a security-relevant transparency failure because users may believe their data stays within their browser sessions when in fact the tool can send prompts and results to an undisclosed third-party service.

Description-Behavior Mismatch

Medium
Confidence
99% confidence
Finding
After collecting responses from browser-authenticated services, the code forwards those results to a separate synthesis API via _cliproxy_chat. This expands data exposure beyond the services the user directly queried and is especially dangerous because the browser-only description does not disclose that page-derived content will be relayed to another external processor.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The synthesis step sends both the user's original query and extracted content from Perplexity/Grok/Gemini to an external API without an explicit warning or consent at the point of use. Because those browser-derived results may contain sensitive user-entered material, account-contextual content, or proprietary research, this creates a meaningful confidentiality and data-handling risk.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Content
def check_cdp(port: int = 9222) -> bool:
    try:
        import httpx
        resp = httpx.get(f"http://127.0.0.1:{port}/json/version", timeout=2)
        return resp.status_code == 200
    except Exception:
        return False
Confidence
70% confidence
Finding
Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Content
def check_cdp(port: int = 9222) -> bool:
    try:
        import httpx
        resp = httpx.get(f"http://127.0.0.1:{port}/json/version", timeout=2)
        return resp.status_code == 200
    except Exception:
        return False
Confidence
70% confidence
Finding
Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Unpinned Dependencies

Low
Category
Supply Chain
Content
httpx>=0.27
websockets>=12.0
Confidence
95% confidence
Finding
The dependency is specified with a lower bound only (httpx>=0.27), which allows non-reproducible installs and may pull in future releases with breaking changes or newly introduced vulnerabilities. In a security-sensitive skill that automates browser-driven research and network access, weak dependency control increases supply-chain risk and makes it harder to verify whether deployed environments are safe.

Unverifiable Dependency: httpx has 2 known advisory(ies) (CVE-2021-41945 (Improper Input Validation in httpx); CVE-2021-41945 (Encode OSS httpx <=1.0.0.beta0 is affected by improper input validation in `http)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
82% confidence
Finding
The manifest does not pin httpx, so it is impossible to determine from this file alone whether an affected version with known advisories could be installed. This uncertainty is itself a supply-chain weakness: the environment may resolve to a vulnerable version, and the lack of reproducibility prevents reliable security assurance for a network-capable skill.

Unpinned Dependencies

Low
Category
Supply Chain
Content
httpx>=0.27
websockets>=12.0
Confidence
95% confidence
Finding
The dependency is unpinned (websockets>=12.0), so installations may resolve to different versions over time, including releases with undiscovered or newly disclosed security issues. Because this skill relies on browser/CDP-style communication and network messaging, dependency drift in a websocket library can directly affect exposure to denial-of-service or protocol-handling flaws.

Unverifiable Dependency: websockets has 4 known advisory(ies) (CVE-2018-1000518 (websockets is vulnerable to denial of service by memory exhaustion); CVE-2021-33880 (Observable Timing Discrepancy in aaugustin websockets library); CVE-2018-1000518 (aaugustin websockets version 4 contains a CWE-409: Improper Handling of Highly C) +1 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
83% confidence
Finding
Because websockets is not pinned, the file cannot prove that installed versions are outside the range of known vulnerable releases. In a tool that performs parallel remote queries and likely depends on persistent browser or websocket communication, uncertainty around websocket library safety increases the chance of avoidable denial-of-service or protocol-level exposure.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
The system prompt says "Write in the query's language," imposing a locale/language behavior automatically based on input rather than offering the user a choice. The policy only allows such constraints when the skill provides explicit language choice or documents a justified locale restriction.

Static analysis

No suspicious patterns detected.