Back to skill

Security audit

Job Hunter

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent job-search helper, but it needs Review because it stores a Gemini API key and job-search history locally in plaintext and sends user/job details to Gemini for scoring.

Install only if you are comfortable with public LinkedIn scraping, local retention of your searches and saved jobs, and sending job listings plus your criteria to Google Gemini when AI scoring is enabled. Avoid putting sensitive personal details in free-text criteria, and treat the Gemini key as exposed if you pass it through the documented command-line setup on a shared or logged system.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/job_hunter.py:86
Finding

Gemini API Key Exposed Through Process Arguments and Insecure Plaintext Storage

Content
View full analysis
None: """Persist *data* as JSON to *path*, creating directories as needed.""" ensure_dir() with open(path, "w") as f: json.dump(data, f, indent=2, ensure_ascii=False) ``` ```python def cmd_setkey(api_key: str) -> None: """Store the Gemini API key in the config file.""" config = load_config() config["gemini_api_key"] = api_key save_json(CONFIG_FILE, config) print(json.dumps({"status": "ok", "message": "Gemini API key saved"}, indent=2)) ``` ```python command = sys.argv[1] commands: dict[str, Any] = { "search": lambda: cmd_search(sys.argv[2]), "setkey": lambda: cmd_setkey(sys.argv[2]), "save": lambda: cmd_save(sys.argv[2]), "saved": lambda: cmd_saved(), "unsave": lambda: cmd_unsave(sys.argv[2]), "history": lambda: cmd_history(), "rerun": lambda: cmd_rerun(sys.argv[2]), } ``` ### Technical Analysis The documented setup procedure passes the Gemini API key as a command-line argument. Depending on the host environment, command-line arguments may be exposed through process inspection, shell history, audit facilities, terminal logs, or Agent execution transcripts. The key is subsequently placed in `config.json` as unencrypted JSON. Although storage of an API credential may be necessary for optional AI scoring, `save_json()` relies entirely on the proces ...[truncated 1342 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/job_hunter.py:443
Finding

Indirect Prompt Injection Through Untrusted LinkedIn Job Descriptions

Content
View full analysis
str: """Format a list of jobs into a text block for the AI scoring prompt.""" lines: list[str] = [] for i, job in enumerate(jobs): parts = [f"Job {i}:", f" Title: {job['title']}", f" Company: {job['company']}"] if job.get("location"): parts.append(f" Location: {job['location']}") if job.get("salary"): parts.append(f" Salary: {job['salary']}") if job.get("description"): parts.append(f" Description: {job['description'][:MAX_DESCRIPTION_FOR_PROMPT]}") parts.append(f" URL: {job['url']}") lines.append("\n".join(parts)) return "\n\n".join(lines) def _build_scoring_prompt(jobs: list[dict[str, Any]], params: dict[str, Any]) -> str: """Build the full Gemini scoring prompt for a batch of jobs.""" return SCORING_PROMPT.format( keywords=params.get("keywords", ""), technologies=", ".join(params.get("technologies", [])) or "Not specified", location=", ".join(params.get("countries", [])) or params.get("location", "Any"), remote="Yes" if params.get("remote") else "No preference", salary_min=f"EUR {params['salary_min']}" if params.get("salary_min") else "Not specified", experience=params.get("experience", "Any"), exclude=", ".join(params.get("exclude", [])) or "None", ai_prompt=params.get("ai_prompt", "No additional requirements"), jobs_text=_format_jobs_for_prompt(jobs), ) ``` ```python def _parse_scores(response_text: str, count: int) -> list[dict[str, Any]]: """Parse AI response text into a list of score dicts, with fallback.""" try: text = response_text.strip() if "```" in text: ...[truncated 3396 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:5
Finding

Unpinned Runtime Dependencies Create Supply-Chain Exposure

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (11)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill explicitly instructs the agent to run Python code that performs network access and writes persistent data, but it declares no tool scope or permissions boundary. That means a user may invoke a skill with capabilities beyond what is transparently disclosed, increasing the risk of unintended scraping, external data transmission, and local persistence without proper consent or sandboxing.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The trigger list is very broad and includes generic phrases like 'hiring', 'job listings', and multilingual variants that can match many ordinary conversations. Over-broad invocation can cause the skill to activate unexpectedly and initiate scraping, history storage, or external API usage in contexts where the user did not clearly intend those actions.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill description and early workflow do not clearly warn that it scrapes LinkedIn and may transmit user criteria and job content to Google Gemini for scoring. This is a transparency and privacy problem because users may disclose sensitive job-seeking preferences, locations, or notes without understanding that third-party services and local storage are involved.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This markdown file includes a gemini_api_key field in the config example, which is a sensitive credential. The document provides no accompanying warning about protecting secrets, avoiding hard-coding real keys, or storing them securely, so users may copy this pattern unsafely.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill creates persistent local storage for config, history, and saved jobs, which materially expands its data-handling behavior beyond a simple search-and-score assistant. This is risky because users may not expect retention of searches, bookmarks, and related metadata on disk, increasing privacy exposure on shared or compromised systems.

Content

No source excerpt is available for this finding.

Ssd 3

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

User-provided free-text criteria is inserted directly into the LLM prompt and also persisted in history, so users may unintentionally store and transmit sensitive personal data such as salary expectations, visa status, health-related accommodation needs, or demographic preferences. In a job-search skill, this context makes the issue more concerning because employment searches often involve highly personal details.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill sends job titles, companies, locations, salary details, descriptions, URLs, and user-supplied criteria to Google Gemini for scoring without an explicit just-in-time disclosure. This creates a privacy and data-governance risk, especially because user free-text criteria may contain personal preferences or sensitive employment details.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The skill stores the Gemini API key in a local JSON config file without manifest disclosure. Local credential persistence is security-relevant behavior because secrets on disk may be exposed to other local users, backups, malware, or accidental exfiltration.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
99% confidence
Finding

The API key is written to disk with no user-facing warning that a sensitive credential will be stored locally. Even if intended for convenience, silent persistence of secrets can lead to credential theft or reuse if the filesystem is accessible to other processes or users.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The sample ai_summary is written in Spanish, which suggests the skill may produce output in a specific language. The file does not indicate that language is user-selectable or that Spanish output is intentionally limited to a justified locale-specific use case.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
98% confidence
Finding

The prompt instructs the model to return the scoring reason in Spanish, which imposes a specific language regardless of user preference. There is no opt-in, language selection mechanism, or documented justification for this locale constraint.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.