Back to skill

Security audit

vryfik skill

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated search-helper purpose, but its web-probing and source-validation safeguards have real flaws that warrant review before installation.

Review this skill before installing in environments with access to internal networks or sensitive search topics. Its local cache is disclosed and bounded, but it stores recent queries and assembled snippets; its source checks should be fixed before relying on them for SSRF prevention or prompt-injection resistance.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/parallel-probe.js:87
Finding
SSRF Protection Can Be Bypassed Through DNS Rebinding and Incomplete IPv6 Filtering<![CDATA[ ## Vulnerability Details **File Location**: `scripts/parallel-probe.js`, lines 87–114 **Vulnerability Type**: Server-Side Request Forgery through DNS time-of-check/time-of-use mismatch **Risk Level**: High ### Vulnerable Code ```javascript let resolvedIp; try { const { address } = await dns.lookup(parsed.hostname); resolvedIp = address; } catch { return { url: rawUrl, available: false, error: 'dns_resolution_failed' }; } if (isBlockedHost(resolvedIp)) { return { url: rawUrl, available: false, error: 'blocked_resolved_ip' }; } const lib = parsed.protocol === 'https:' ? https : http; const domain = parsed.hostname.replace(/^www\./, ''); const tier = DOMAIN_TIER[domain] || 'unknown'; const score = REP.scores[tier] || REP.scores.unknown; return new Promise(resolve => { const options = { method: 'HEAD', hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.pathname + parsed.search, timeout: TIMEOUT_MS, headers: { 'User-Agent': 'search-skill-probe/1.0 (availability-check)' } }; const req = lib.request(options, res => { ``` The associated address blocklist is incomplete: ```javascript const BLOCKED_HOST_PATTERNS = [ /^localhost$/i, /^127\./, /^0\.0\.0\.0$/, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^169\.254\./, /^::1$/, /^fc[0-9a-f]{2}:/i, /^fe80:/i, /^0::/, ]; ``` ### Technical Analysis The code resolves the supplied hostname with `dns.lookup()` and checks the resulting address against a private-address blocklist. However, the validated address is not used to establish the connection. Instead, `http.request()` or `https.request()` receives the original hostname and may perform another DNS resolution. This creates a time-of-check/time-of-use gap. An attacker who controls DNS for a supplied hostname can return a public address during the explicit validation lookup and an internal address when the HTTP ...[truncated 2061 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve all addresses for the hostname and reject the request if any resolved address is non-public. 2. Use `net.isIP()` and a CIDR-aware IP classification library or equivalent robust implementation rather than regular-expression matching. 3. Block all loopback, private, link-local, unspecified, multicast, reserved, documentation, carrier-grade NAT, and IPv4-mapped IPv6 address ranges as appropriate for the deployment. 4. Pin the validated address to the request by supplying a custom `lookup` function that returns only the previously validated address. 5. For HTTPS, preserve the original hostname for TLS SNI and certificate validation while connecting only to the pinned address. 6. Revalidate the connected socket's remote address after connection establishment and terminate the connection if it differs from the approved address. 7. Restrict destination ports to an explicit allowlist, normally ports 80 and 443, unless other ports are required. 8. Add regression tests for DNS rebinding, IPv4-mapped IPv6, `fd00::/8`, loopback, link-local, and cloud metadata destinations. 9. Correct the security manifest so it does not claim complete DNS-rebinding prevention until address pinning is implemented. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/credibility-arbiter.js:105
Finding
Caller-Supplied Domain Can Spoof Source Credibility<![CDATA[ ## Vulnerability Details **File Location**: `scripts/credibility-arbiter.js`, lines 105–108 **Vulnerability Type**: Improper trust-boundary validation **Risk Level**: Medium ### Vulnerable Code ```javascript const results = sources.slice(0, 10).map(src => { const domain = src.domain || extractDomain(src.url || ''); const { tier, score: domainScore } = scoreDomain(domain); const contentMult = scoreContent(src.content); const finalScore = +(domainScore * contentMult).toFixed(3); return { domain, tier, domainScore, contentMultiplier: +contentMult.toFixed(3), finalScore, verdict: finalScore >= 0.7 ? 'trust' : finalScore >= 0.4 ? 'verify' : 'reject' }; }); ``` ### Technical Analysis The credibility arbiter accepts both a source URL and an optional caller-provided `domain`. When `domain` is present, it takes precedence over the hostname derived from the URL. No check confirms that the claimed domain corresponds to the URL. Consequently, a source hosted on an attacker-controlled domain can claim a highly trusted domain from `domain-reputation.json`, such as `github.com`, and receive that domain's score. The final verdict is then calculated using the forged identity. For example, input equivalent to the following can receive the reputation of GitHub even though the content originates elsewhere: ```json { "sources": [{ "url": "https://attacker.example/malicious-content", "domain": "github.com", "content": "Example implementation for 2026" }] } ``` The content heuristics may further increase the resulting score because terms such as `example`, dates, or code markers are treated as positive signals. ### Attack Path 1. An attacker causes a search result or pipeline object to contain a malicious URL. 2. The source object includes a forged `domain` property naming a trusted site. 3. The arbiter uses the supplied `domain` instead of parsing the URL. 4. `scoreDomain()` assigns the trusted site's reputa ...[truncated 787 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Always derive the authoritative hostname from `src.url`. 2. Remove the caller-controlled `domain` field if it is unnecessary. 3. If `domain` must be retained as metadata, reject the source when it does not exactly match the normalized URL hostname. 4. Normalize hostnames by lowercasing them, removing a trailing dot, converting internationalized names consistently, and handling the `www.` prefix according to a documented policy. 5. Use a public-suffix-aware library when comparing registrable parent domains; do not assume that the final two labels always form the registrable domain. 6. Do not increase credibility based solely on easily forged content markers such as dates, the word `example`, or code fences. 7. Bind credibility results to the validated URL and pass that same immutable object through probing, arbitration, and assembly. 8. Add tests proving that `https://attacker.example` cannot receive the reputation of a supplied `github.com` domain. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/stream-assembler.js:121
Finding
Untrusted Web Fragments Are Returned and Persisted Without Prompt-Injection Isolation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/stream-assembler.js`, lines 121–129 **Vulnerability Type**: Stored indirect prompt-injection exposure **Risk Level**: Medium ### Vulnerable Code The assembler copies source fragments directly into the answer: ```javascript const sections = unique.map((frag, i) => { const header = formatFragmentHeader(frag, i, lang); const content = (frag.content || frag.snippet || '').trim().slice(0, 2000); return `${header}\n\n${content}`; }); const answer = sections.join('\n\n---\n\n'); const tokensEstimate = estimateTokens(answer); ``` The resulting answer is subsequently persisted by `scripts/semantic-cache.js`: ```javascript active.push({ query: query.slice(0, 300), intent, vec: queryToVector(query, intent), result: String(result).slice(0, MAX_RESULT_LEN), ts: now }); ``` The pipeline instructions in `SKILL.md` direct the Agent to return this assembled `answer` and then cache it after successful assembly. ### Technical Analysis Web search snippets and fragments are untrusted external content. The assembler performs deduplication, truncation, and basic coherence scoring, but it does not distinguish factual source material from text written as instructions to an AI Agent. An attacker can publish search-indexed content containing directives such as requests to ignore prior instructions, disclose data, invoke tools, or misrepresent source material. If that content appears in a search fragment, it is copied verbatim into the answer. The same text can then be stored in the semantic cache and returned for future sufficiently similar queries. Credibility scoring does not address this threat. A reputable hosting domain may contain user-controlled content, and the domain-spoofing vulnerability can independently help hostile content obtain a trusted verdict. Truncating a fragment to 2,000 characters also does not neutralize instruction content. Whether the injected text can cause tool e ...[truncated 1661 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat every search fragment as untrusted data, regardless of domain reputation. 2. Do not return raw fragments as the final answer. Generate a constrained factual synthesis that excludes Agent-directed instructions. 3. Wrap quoted source text in explicit data delimiters and ensure the host prompt states that content inside those delimiters must never be followed as instructions. 4. Detect and reject fragments containing common instruction-hijacking patterns, tool invocation requests, requests for secrets, or attempts to override system and user directives. 5. Maintain source attribution and distinguish quotations from generated conclusions. 6. Cache structured records containing source URLs, timestamps, trust decisions, and sanitized factual extracts rather than the raw assembled answer. 7. Revalidate cached entries before use when source trust policies or sanitization versions change. 8. Add a sanitizer-version field to cache records and invalidate records produced by older policies. 9. Add adversarial tests using fragments that contain instruction-override text, fake system messages, tool requests, and data-exfiltration prompts. 10. Ensure host-side orchestration treats all script output and web content as untrusted data rather than higher-priority instructions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:129
Finding
Credibility Arbitration Pipeline Uses an Incompatible Input Field<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 129–136 **Vulnerability Type**: Security-control integration failure **Risk Level**: Medium ### Vulnerable Configuration The documented pipeline invokes the arbiter using a `results` property: ```markdown ### Step 7 — Score Credibility ``` node scripts/credibility-arbiter.js '{"results":[<probe_results_array>]}' ``` If **all** sources score `< 0.4`, discard everything and tell the user no reliable source was found. Do not assemble. ``` However, `scripts/credibility-arbiter.js` reads only `input.sources`: ```javascript try { const input = JSON.parse(arg); console.log(JSON.stringify(arbitrate(input.sources))); } catch (e) { console.log(JSON.stringify({ error: e.message })); process.exit(1); } ``` ### Technical Analysis The pipeline contract and the executable implementation use incompatible property names. Following `SKILL.md` exactly passes probe data under `results`, while the arbiter invokes `arbitrate(input.sources)`. Since `input.sources` is undefined, arbitration returns: ```json {"error":"sources must be a non-empty array"} ``` The documented pipeline does not explicitly require termination when this error occurs. As a result, the credibility scoring control may fail closed through an aborted workflow or fail open if the host continues to assembly without interpreting the error correctly. This is a security-relevant integration defect because Step 7 is intended to reject collections where all sources have scores below 0.4. ### Attack Path 1. The host follows the command documented in `SKILL.md`. 2. Probe results are supplied under the `results` property. 3. The arbiter looks for the nonexistent `sources` property. 4. Credibility arbitration returns an error instead of source scores. 5. If the host does not explicitly stop on this error, it may proceed to assembly without enforcing the all-sources-below-0.4 rejection rule. 6. Low-credibility or hostile fragments may ...[truncated 584 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Standardize the arbiter input schema. Either change `SKILL.md` to pass `{"sources":[...]}` or update the script to accept `results`. 2. Define and validate a formal schema for data passed between every pipeline stage. 3. Make orchestration fail closed: any probe or arbitration error must stop assembly and prevent cache writes. 4. Return a consistent nonzero exit status for invalid security-control input. 5. Add an end-to-end test that executes the exact commands documented in `SKILL.md`. 6. Test that sources below the credibility threshold cannot reach the assembler. 7. Test malformed, missing, and incorrectly named fields to verify deterministic fail-closed behavior. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (13)

Ae1

High
Category
analysis-evasion
Content
node scripts/stream-assembler.js '{"fragments":[<trusted_fragments>],"query":"<original_query>"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/stream-assembler.js '{"fragments":[<trusted_fragments>],"query":"<original_query>"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger description is broad enough to match many normal queries about web search, documentation, pricing, and current information, which can cause the skill to activate in more contexts than necessary. In a skill with shell, network, and local read/write permissions, unintended invocation increases exposure to side effects such as persistent storage of queries and outbound probes.

Session Persistence

Medium
Category
Rogue Agent
Content
bins: ["node"]  # required runtime; scripts are invoked via shell tool

io:
  cache_path: "~/.antigravity/search-cache/"   # semantic-cache.js read/write scope
                                               # stores query text + result snippets (0o600)
                                               # created automatically on first write
  network_method: "HEAD only"                  # parallel-probe.js; no response body stored
Confidence
88% confidence
Finding
The skill persistently stores query text and result snippets in a local cache under the user's home directory, creating session persistence of potentially sensitive search content. Even with restrictive file permissions, retained queries can expose private interests, internal research topics, API lookups, or other sensitive context to later processes or local compromise.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The overview section presents core operational instructions in Chinese without indicating that language choice is optional or user-selected. This can violate language or locale policy because the skill imposes a specific language presentation without documented opt-in or justification.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The web_search rules include very broad conversational keywords such as "why", "compare", and "vs" that commonly appear in ordinary user questions unrelated to live web retrieval. This can cause unintended routing to web search, exposing user queries to external systems, increasing data leakage risk, and producing behavior that is broader than the user intended.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The natural-language logic in detectLang returns 'zh' for Han characters and 'en' for everything else, which effectively forces an English locale for many users and languages. This is a language-policy concern because the file does not offer a user choice or document that the tool is intentionally limited to English and Chinese.

Session Persistence

Medium
Category
Rogue Agent
Content
* Semantic Cache — Vector similarity cache backed by local JSON file
 *
 * ClawHub Security: FILE SYSTEM ONLY.
 * Read/write limited to CACHE_DIR (default: ~/.antigravity/search-cache/).
 * No network access. No eval. No dynamic code.
 *
 * Commands:
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Natural-Language Policy Violations

Low
Confidence
71% confidence
Finding
The file embeds mixed English and Chinese trigger vocabularies throughout the routing rules, but does not indicate whether language selection is user-driven or whether the skill is intentionally limited to these locales. This can reflect an implicit language policy choice without explicit opt-in or justification.

Intent-Code Divergence

Low
Confidence
95% confidence
Finding
The module documentation explicitly states the skill is 'Pure synchronous computation.' However, the stdin mode implemented later uses asynchronous event listeners on process.stdin ('data' and 'end'), so the documented execution model contradicts the actual code behavior. This is a documentation-to-code mismatch, though it does not introduce a major security impact by itself.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The code contains a hardcoded Chinese-to-English bridging path and only special-cases the 'zh' locale, while all other languages fall back to no equivalent handling. This is a natural-language locale preference embedded in the skill behavior without any user opt-in mechanism or documented justification for why Chinese is singled out.

Natural-Language Policy Violations

Low
Confidence
85% confidence
Finding
The stopword list embeds specific language handling for English and Chinese, which affects how user input is interpreted and cached. Because the file provides no user choice or documented locale constraint, this is a natural-language policy concern under the language/locale rule.

Natural-Language Policy Violations

Low
Confidence
82% confidence
Finding
The code sets `lang = 'en'` by default, which imposes a specific language when the caller does not provide a preference. The policy allows language constraints when the user is offered a choice or opts in, but here the default silently selects English.

Static analysis

No suspicious patterns detected.