Back to skill

Security audit

Skills for finding a job on hh

Security checks for vulnerabilities and agentic risk

Overview

This job-search skill is mostly coherent, but its authenticated hh.ru browser automation is too broadly scoped and can operate on the wrong tab or unsafe URL inputs.

Install only if you intentionally want Russian-market job-search automation with hh.ru Browser Relay. Use a dedicated browser profile logged into only the intended hh.ru account, review URLs before running batch apply, keep auto-apply and outreach disabled unless you explicitly want them, and treat CSV exports as untrusted before opening them in spreadsheet software.

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/hh_apply_batch.py:187
Finding
Unrestricted Browser Navigation Allows JavaScript URL Execution in an Authenticated Tab<![CDATA[ ## Vulnerability Details **File Location**: `scripts/hh_apply_batch.py:187-196` **Supporting Sink**: `scripts/hh_browser_cli.py:89-96` **Vulnerability Type**: Missing URL scheme, hostname, and browser-tab validation **Risk Level**: High ### Vulnerable Code ```python browser = BrowserCli(profile=args.profile) try: browser.ensure_ready() target_id = browser.current_target() current = browser.current_page(target_id).result or {} origin = str(current.get("origin") or "https://hh.ru") results: list[dict[str, Any]] = [] for url in args.urls: normalized = re.sub(r"^https?://[^/]+", origin, url) browser.navigate_js(normalized, target_id) ``` The navigation sink is: ```python def navigate_js(self, url: str, target_id: str | None = None) -> BrowserResult: payload = json.dumps(url, ensure_ascii=False) try: return self.evaluate(f"() => {{ window.location.href = {payload}; return {{navigatingTo: {payload}}}; }}", target_id) except BrowserCliError as e: msg = str(e) if "Execution context was destroyed" in msg or "ERR_ABORTED" in msg: return BrowserResult({"ok": True, "result": {"navigatingTo": url}}) raise ``` The target selection also uses the first attached tab without validating its origin: ```python def current_target(self) -> str: tabs = self.tabs().get("tabs") or [] if not tabs: raise BrowserCliError(f"browser profile {self.profile!r} has no attached tabs") return tabs[0]["targetId"] ``` ### Technical Analysis The application workflow states that positional arguments are HH vacancy URLs, but it does not parse or validate their URL scheme or hostname. The regular expression only rewrites values beginning with an HTTP or HTTPS origin: ```python re.sub(r"^https?://[^/]+", origin, url) ``` Consequently, a non-HTTP value such as `javascript:<payload>` remains unchanged. The value is then assigned to `window.location.href` inside an attac ...[truncated 2665 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every supplied URL with `urllib.parse.urlsplit()` before navigation. 2. Require the `https` scheme and reject all other schemes, including: - `javascript` - `data` - `file` - `blob` - `vbscript` 3. Enforce an explicit hostname allowlist, such as `hh.ru` and specifically approved HH subdomains. 4. Reject URLs containing: - Embedded usernames or passwords - Unexpected ports - Backslash-based authority confusion - Empty or malformed hostnames 5. Verify the final parsed URL again after any normalization. 6. Do not derive the navigation origin from an arbitrary currently selected tab. 7. Enumerate attached tabs and explicitly select one whose parsed hostname belongs to the HH allowlist. 8. Abort if no verified HH tab is attached. 9. Add a defense-in-depth check in `BrowserCli.navigate_js()` so dangerous schemes are rejected even if a caller fails to validate them. 10. Add regression tests for `javascript:`, `data:`, mixed-case schemes, whitespace-prefixed schemes, malformed authorities, unrelated domains, and an unrelated first tab. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/export_shortlist.py:31
Finding
Spreadsheet Formula Injection in Shortlist CSV Exports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export_shortlist.py:31-37` **Vulnerability Type**: CSV formula injection **Risk Level**: Medium ### Vulnerable Code ```python out = Path(sys.argv[2]) out.parent.mkdir(parents=True, exist_ok=True) with out.open("w", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) writer.writeheader() for v in rows: writer.writerow({k: getattr(v, k) for k in FIELDS}) ``` The exported fields include externally sourced text: ```python FIELDS = ["source", "company", "title", "source_url", "fit_label", "fit_score", "salary_min", "salary_max", "salary_currency", "remote_mode", "seniority"] ``` ### Technical Analysis Vacancy values can originate from externally controlled postings, including company names, titles, source URLs, and other textual fields. These values are written directly into CSV cells without neutralizing spreadsheet formula prefixes. CSV quoting only preserves the field structure. It does not prevent spreadsheet software from treating a cell beginning with characters such as the following as a formula: ```text = + - @ ``` Some spreadsheet applications may also recognize formula-like content after leading tabs, carriage returns, line feeds, or other whitespace. Therefore, an attacker-controlled vacancy value can remain inert during parsing and scoring but become executable when the generated shortlist is opened in spreadsheet software. ### Attack Path 1. An attacker publishes or supplies a vacancy containing a formula-like title, company name, or other exported value, for example: ```text =HYPERLINK("https://attacker.example/track","Senior Engineer") ``` 2. The vacancy is parsed and stored as ordinary text. 3. The vacancy meets the shortlist score or label threshold. 4. `export_shortlist.py` passes the value directly to `csv.DictWriter`. 5. The user opens the resulting CSV in Excel, LibreOffice Calc, or another formula-aware spreadshee ...[truncated 1014 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize every text value before passing it to `csv.DictWriter`. 2. Detect dangerous prefixes after accounting for leading whitespace, tabs, carriage returns, and line feeds. 3. Prefix formula-like cells with a single quote or another application-appropriate neutralization character. 4. Apply sanitization to all externally influenced text fields, not only the title and company. 5. Preserve numeric fields as validated numeric types rather than converting arbitrary strings into spreadsheet cells. 6. Consider generating XLSX output with cells explicitly typed as strings when spreadsheet compatibility is required. 7. Document that CSV output contains untrusted external vacancy data. 8. Add tests for values beginning with `=`, `+`, `-`, `@`, tabs, carriage returns, line feeds, and whitespace followed by a formula prefix. A defensive helper can follow this pattern: ```python def safe_csv_cell(value): if not isinstance(value, str): return value if value.lstrip("\t\r\n ").startswith(("=", "+", "-", "@")): return "'" + value return value ``` The helper should be applied to every exported field before calling `writer.writerow()`. ]]>

T08 · Insecure Dependencies

Note
Location
scripts/requirements.txt:1
Finding
Mutable Dependency Ranges Allow Unreviewed Package Versions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/requirements.txt:1-2` **Supporting Location**: `SKILL.md:165-169` **Vulnerability Type**: Unpinned and unhashed third-party dependencies **Risk Level**: Low ### Vulnerable Configuration ```text pydantic>=2,<3 rapidfuzz>=3,<4 ``` The documented installation command resolves these ranges directly: ```bash python -m pip install -r skills/job-search/scripts/requirements.txt ``` ### Technical Analysis The dependency file permits any future `pydantic` 2.x or `rapidfuzz` 3.x release and does not include artifact hashes. As a result, two installations performed at different times can install different code even when the Skill package itself has not changed. This does not prove that the current packages are malicious. The issue is that dependency selection is mutable and the installed artifacts are not cryptographically constrained to reviewed versions. A compromised upstream account, malicious release, dependency-host compromise, or incompatible future version could introduce unreviewed behavior into the Skill environment. Python packages execute code when imported, and package installation can also execute build-related code in some distribution scenarios. Consequently, a compromised dependency may obtain the privileges of the user running the installation or Skill scripts. ### Attack Path 1. A new package version is published within one of the accepted version ranges. 2. The release is compromised, malicious, or contains a security regression. 3. A later user follows the documented `pip install -r` command. 4. Package resolution selects the new version because no exact version or artifact hash is specified. 5. The unreviewed code is installed into the Skill's Python environment. 6. The dependency executes during installation, import, validation, or fuzzy-matching operations. 7. The code receives the filesystem, process, and network access available to the Python process. ### Impact Assessment ...[truncated 652 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin each dependency to an exact reviewed version. 2. Generate and maintain a lock file using a controlled tool such as `pip-tools`, Poetry, or another reproducible dependency manager. 3. Record SHA-256 hashes for every accepted distribution artifact. 4. Install with hash verification, such as: ```bash python -m pip install --require-hashes -r requirements.txt ``` 5. Review transitive dependencies in addition to the two direct dependencies. 6. Prefer binary wheels from trusted package indexes where appropriate, and avoid unexpected source builds. 7. Use a dedicated virtual environment with minimal filesystem and network privileges. 8. Update pinned versions through a documented review process with automated vulnerability scanning and tests. 9. Configure the package installer to use an explicit trusted index rather than inheriting arbitrary index configuration from the environment. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (27)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill includes browser/account actions on hh.ru resumes, specifically automated 'raise resume' behavior after refresh and post-apply steps, but that capability is not clearly disclosed in the top-level description. Undisclosed account-modifying automation is risky because it can perform externally visible actions on a user's behalf beyond what a reviewer may reasonably infer from a generic job-search skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill includes browser/account actions on hh.ru resumes, specifically automated 'raise resume' behavior after refresh and post-apply steps, but that capability is not clearly disclosed in the top-level description. Undisclosed account-modifying automation is risky because it can perform externally visible actions on a user's behalf beyond what a reviewer may reasonably infer from a generic job-search skill.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill clearly instructs use of file reads/writes and shell commands (for project creation, logging, dependency installation, and running bundled scripts) but does not declare an explicit tool scope or allowed-tools policy. That creates an authorization gap: a runtime may grant broader capabilities than reviewers or users expect, increasing the chance of unintended filesystem changes or command execution.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
4. Click the OpenClaw Browser Relay extension icon on that tab so the badge turns ON.
5. Tell the agent the relay is attached, then continue.

Explain why only briefly: Browser Relay lets the agent work with the user’s already logged-in tab without asking for passwords.

## What to do after relay setup
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## When not to push Browser Relay

Do not ask the user to set up Browser Relay unless the task actually needs logged-in browser automation.

Examples that usually do NOT need relay:
- parsing a resume
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Natural-Language Policy Violations

Medium
Confidence
89% confidence
Finding
The document explicitly prioritizes sources for the 'current target market' and then defines that market as Russian via references like 'the Russian market' and Russian-local platforms. This is a natural-language locale policy constraint presented as a fixed strategy, with no indication that the user can choose a different language or regional market.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
normalizer = Path(__file__).with_name("normalize_vacancy.py")
    rows = []
    for path in sorted([p for p in input_dir.iterdir() if p.is_file()]):
        proc = subprocess.run([sys.executable, str(normalizer), source, str(path)], capture_output=True, text=True, check=True)
        rows.append(json.loads(proc.stdout))

    out_path.parent.mkdir(parents=True, exist_ok=True)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
rows = []
    for path in sorted(posts_dir.glob("*.txt")):
        proc = subprocess.run([sys.executable, str(parser), str(path)], capture_output=True, text=True, check=True)
        rows.append(json.loads(proc.stdout))

    out_path.parent.mkdir(parents=True, exist_ok=True)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding
Suspicious Unicode normalization or mixed-script content

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This code invokes an external CLI via subprocess to control a browser profile, but there is no confirmation prompt, user-facing log/print, or explanatory comment/docstring disclosing that an external command will be executed. Because this file is a code file and subprocess execution is a safety-relevant operation under the rule, the absence of any visible disclosure is a quality/safety issue.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run(self, args: list[str]) -> dict[str, Any]:
        cmd = [OPENCLAW_BIN, "browser", "--browser-profile", self.profile, "--timeout", str(self.timeout_ms), "--json", *args]
        proc = subprocess.run(cmd, capture_output=True, text=True)
        if proc.returncode != 0:
            raise BrowserCliError(proc.stderr.strip() or proc.stdout.strip() or f"browser command failed: {' '.join(cmd)}")
        out = proc.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This wrapper provides direct browser navigation and JavaScript evaluation capabilities against an attached browser profile, including the ability to run arbitrary page-context code via the fn parameter. In a job-search skill, that is more dangerous because the workflow interacts with live third-party sites and potentially authenticated sessions, so an upstream prompt injection or untrusted input could drive unintended actions, scrape sensitive data, or operate on behalf of the user without meaningful consent boundaries.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The script uses browser automation to access hh.ru resumes, collects the full resume card data, and prints it to stdout as JSON. Even though the immediate purpose is to compute the next resume raise time, emitting the full `cards` structure may expose personal or account-linked data in logs, terminals, CI output, or downstream tooling without clear minimization or disclosure.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding
Suspicious Unicode normalization or mixed-script content

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The file contains multiple user-facing strings in Russian, including the salary fallback at L052, default notes/titles in L059-L076, and output text in L096. This enforces a specific locale for users without opt-in or a documented region-specific justification, which matches the language/locale policy violation criteria.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding
Suspicious Unicode normalization or mixed-script content

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This code emits multiple user-facing strings in Russian, including salary/status text, report headings, and card labels, while the CLI offers no option to select language or locale. That creates a natural-language policy concern because the skill forces a specific language without user opt-in or justification.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding
Suspicious Unicode normalization or mixed-script content

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The script hard-codes user-facing Russian strings such as fallback labels and section headings, which means output is always partly in Russian regardless of user preference. This is a natural-language locale policy concern because the file provides no opt-in, language selection, or justification for restricting output to a specific language.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script writes candidate profile data and vacancy contents to named temporary files on disk using delete=False, which can expose potentially sensitive personal and employment data to other local processes, backups, or forensic recovery if cleanup fails. In a job-search skill, this is more sensitive than generic temp data because profiles may contain PII, compensation expectations, contact details, and work history.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
with tempfile.NamedTemporaryFile("w", suffix=".json", encoding="utf-8", delete=False) as vf:
                    json.dump(vacancy, vf, ensure_ascii=False, indent=2)
                    vacancy_path = vf.name
                proc = subprocess.run([sys.executable, str(score_script), profile_path, vacancy_path], capture_output=True, text=True, check=True)
                score = json.loads(proc.stdout)
                vacancy.update(score)
                dst.write(json.dumps(vacancy, ensure_ascii=False) + "\n")
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
try:
        script = Path(__file__).with_name("job_match_score.py")
        subprocess.run([sys.executable, str(script), tmp_path, vacancy_json], check=True)
    finally:
        Path(tmp_path).unlink(missing_ok=True)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for v in rows:
            writer.writerow({k: getattr(v, k) for k in FIELDS})

    print(f"Exported {len(rows)} shortlisted vacancies -> {out}")
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Natural-Language Policy Violations

Low
Confidence
80% confidence
Finding
The script hard-codes 'Asia/Krasnoyarsk' as the default timezone, imposing a specific locale assumption unless the user overrides it. The file does not explain or justify why this locale is the default or prompt the user to choose one.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The script uses browser automation to navigate to hh.ru and extract resume-related page content from the current authenticated browser session, but there is no built-in user disclosure, consent check, or scope validation in the code path. In a job-search skill this may be expected behavior, but it still creates privacy risk because sensitive account data can be read silently if the tool is invoked in the wrong context or against an unintended profile.

Static analysis

No suspicious patterns detected.