Back to skill

Security audit

Досье на человека

Security checks for vulnerabilities and agentic risk

Overview

This skill is not clearly malicious, but it deserves review because it broadly creates dossiers on living people, sends personal research data to a third-party API, persists search/query artifacts, and fetches cited URLs without strong network scoping.

Install only if you are comfortable using a Russian-only tool that profiles living people for a legitimate meeting or due-diligence purpose, sends dossier queries and fact checks to Perplexity, and keeps local query/cache artifacts. Avoid using it for private individuals, stalking, harassment, discrimination, or sensitive personal investigations, and run it only in an environment where outbound network access cannot reach internal services.

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 (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/verify_facts.py:427
Finding

Server-Side Request Forgery in Source Verification

Content
View full analysis

Vulnerability Details

File Location: scripts/verify_facts.py, lines 427–438 and 454–459
Vulnerability Type: Server-Side Request Forgery (SSRF)
Risk Level: Medium

Vulnerable code:

python
def fetch_source_text(url: str, timeout: float = 20.0) -> tuple[str, str]:
    """Текст страницы источника. Возвращает (текст, причина отказа)."""
    timeout = min(timeout, time_left())
    if timeout < 3:
        return "", "не хватило времени до дедлайна"
    try:
        req = urllib.request.Request(url, headers={
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                          "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36",
            "Accept-Language": "ru,en;q=0.8",
        })
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read(1_500_000)
python
urls = [str((srcmap.get(str(n)) or {}).get("url") or "") for n in claim.get("sources") or []]
urls = [u for u in urls if u.startswith("http")]
if not urls:
    return "НЕ ПРОВЕРИТЬ", "у источника нет адреса"
reasons = []
for url in urls[:2]:
    text, err = fetch_source_text(url)

Technical Analysis

verify_facts.py extracts source URLs from the dossier JSON and directly passes them to urllib.request.urlopen. The only URL restriction is a string-prefix check for http, which does not establish that the destination is a public HTTP or HTTPS service.

There is no validation of:

  • The URL scheme after parsing.
  • Loopback addresses such as 127.0.0.1 or ::1.
  • Private network ranges.
  • Link-local addresses and cloud metadata endpoints.
  • Reserved, multicast, or otherwise non-public addresses.
  • DNS results before connection.
  • Redirect destinations followed by urlopen.
  • DNS rebinding between validation and connection.

The source URL is part of dossier research data and can originate from externally sou ...[truncated 2013 chars]

Remediation
View remediation

Remediation Suggestions

  1. Parse URLs with urllib.parse.urlsplit and allow only exact http and https schemes.
  2. Resolve the hostname before connecting and reject every resolved address in loopback, private, link-local, multicast, unspecified, reserved, and non-global ranges for both IPv4 and IPv6.
  3. Disable automatic redirects, or validate every redirect destination using the same scheme, hostname, DNS, and IP-address policy before following it.
  4. Address DNS rebinding by ensuring the validated IP is the address used for the connection, or by enforcing equivalent restrictions through a trusted egress proxy.
  5. Reject URLs containing ambiguous hostname encodings, embedded credentials, malformed ports, or unsupported address representations.
  6. Consider an allowlist of expected public source domains if the workflow permits it.
  7. Apply outbound firewall rules that prevent the Skill runtime from reaching internal and metadata networks.
  8. Return generic fetch failures so internal reachability cannot be distinguished through detailed network errors.
  9. Add tests covering direct and redirected requests to IPv4 and IPv6 loopback, RFC1918 private ranges, link-local ranges, integer or encoded IP representations, and DNS names resolving to non-public addresses.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (27)

Tainted flow: 'req' from os.environ.get (line 75, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/search.py (reported line 79)May include surrounding context.

python
SONAR_URL, data=json.dumps(body).encode(),
            headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                payload = json.load(resp)
            answer = str((payload.get("choices") or [{}])[0].get("message", {}).get("content") or "")
            urls = []

Tainted flow: 'req' from os.environ.get (line 565, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Функция fetch_source_text() без ограничений доверяет URL из входного dossier JSON и открывает их через urllib.request.urlopen. Это позволяет злоумышленнику подложить ссылки на внутренние сервисы, localhost или чувствительные сетевые адреса и превратить навык с разрешением net в SSRF-прокси для сетевого сканирования или доступа к внутренним ресурсам.

Content

Scanner excerpt · scripts/verify_facts.py (reported line 438)May include surrounding context.

python
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36",
            "Accept-Language": "ru,en;q=0.8",
        })
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read(1_500_000)
        html = raw.decode("utf-8", "replace")
    except Exception as exc:  # noqa: BLE001

Tainted flow: 'req' from os.environ.get (line 565, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/verify_facts.py (reported line 569)May include surrounding context.

python
SONAR_URL, data=json.dumps(body).encode(),
            headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=per_try) as resp:
                payload = json.load(resp)
            break
        except urllib.error.HTTPError as exc:

Tainted flow: 'req' from os.environ.get (line 565, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/verify_facts.py (reported line 635)May include surrounding context.

python
"""
    req = urllib.request.Request("https://api.perplexity.ai/", method="HEAD")
    try:
        with urllib.request.urlopen(req, timeout=timeout):
            return ""
    except urllib.error.HTTPError:
        return ""

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

Описание навыка говорит о полноценном пайплайне OSINT-досье с сетевым поиском, верификацией фактов и рендерингом итогового документа. Однако данный кодовый фрагмент реализует только вспомогательный справочник: определяет корневой каталог навыка, читает markdown-файлы/разделы с диска и печатает инструкции по использованию других скриптов. Он использует файловую систему только для чтения и не использует сеть, не обрабатывает данные о человеке и не производит итоговый артефакт. Это не просто поддерживающая деталь заявленного поведения, а materially different purpose для данного фрагмента: документация/навигация вместо выполнения описанной основной функции.

Content

No source excerpt is available for this finding.

Vague Triggers

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

Условие активации предельно широкое: навык должен открываться на любой запрос о конкретном живом человеке. В контексте инструмента, который специально собирает досье, это резко увеличивает вероятность массового или необоснованного профилирования людей без проверки необходимости, согласия, роли субъекта или чувствительности запроса.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

Навык описывает производство досье на живых людей как стандартный output, но не требует явного предупреждения о приватности, рисках ошибочной идентификации, вторичном использовании и потенциальном вреде субъекту данных. Для OSINT-профилирования это опасно, потому что нормализует сбор и упаковку персональных сведений в удобный для передачи формат без достаточных safeguards.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
88% confidence
Finding

The script reads environment variables such as OUROBOROS_SKILL_STATE_DIR and OUROBOROS_TASK_DRIVE to determine trusted read/write locations and behavior. If the permission model does not declare env access, this creates a capability mismatch: the skill can be influenced by undeclared environment state, which may redirect file writes, alter trust boundaries, or weaken the operator’s understanding of what the skill can access.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

Навык системно инструктирует собирать досье на живых людей, включая год и место рождения, гражданство/проживание, карьерную хронологию, публичные позиции, споры и санкции. Даже при оговорках об открытых источниках это создаёт масштабируемый механизм профилирования и агрегации персональных данных, часть которых чувствительна или не необходима для деловой встречи, что повышает риск вторжения в приватность, дискриминации и misuse против конкретного человека.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

Документация побуждает оператора выполнять shell-команду для поиска task_id и навигации по внутренним каталогам платформы. Это расширяет поведение навыка за пределы заявленной функции подготовки досье и приучает к ручному исследованию файловой структуры окружения, что повышает риск избыточного доступа к локальным данным и ошибок обращения к чужим рабочим каталогам.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
99% confidence
Finding

Lines L870-L878 state 'Язык: только русский' and require the dossier to be entirely in Russian except for organization names and abbreviations. This forces a specific language regardless of user preference, and the file does not justify the restriction as a region-specific or compliance-driven requirement.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The document title and all operating instructions are written in Russian, and the file does not indicate that the user may choose another language or that the locale restriction is required for a region-specific purpose. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

This Python file contains natural-language instructions and help text that force a specific language for all users. Under the stated policy, language/locale constraints should offer user opt-in or be clearly justified as region-specific; neither is present here.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The comments and implementation explicitly convert month names from any language into Russian because the dossier is assumed to be read by a Russian-speaking executive. This hard-codes a language/locale behavior rather than offering a user choice or documenting a justified region-specific constraint in a way the user can opt into.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Ssd 3

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The script persistently logs every search query to a local JSONL file, and this skill is specifically designed to build dossiers on living people before business meetings. Those queries can contain personal data, sensitive investigative terms, or confidential business context; retaining them by default increases privacy, insider-threat, and secondary-disclosure risk if the workspace is shared or later accessed.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
90% confidence
Finding

The skill transmits user-supplied search content to an external third-party service (Perplexity Sonar) over the network. In this specific dossier-gathering context, queries are likely to include names of living people and potentially sensitive business or investigative details, so sending them off-host creates a real confidentiality and privacy exposure even though the transmission is part of the feature's intended behavior.

Content

Scanner excerpt · scripts/search.py (reported line 30)May include surrounding context.

python
sys.dont_write_bytecode = True

SONAR_URL = "https://api.perplexity.ai/chat/completions"
MAX_QUERIES = 8          # волна — до восьми запросов, как в методике
MAX_ANSWER_CHARS = 3500  # ответ на один запрос; дисциплина вывода
LOG_NAME = "search_log.jsonl"

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The system prompt explicitly instructs the external model to 'Отвечай по-русски' ('Answer in Russian'), which imposes a specific language choice. The policy allows locale constraints only when they are optional or clearly justified as region-specific, and this file does not offer user choice or opt-in.

Content

No source excerpt is available for this finding.

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
88% confidence
Finding

Скрипт отправляет во внешний сервис Perplexity текст утверждений из досье, которые могут содержать персональные данные о живом человеке. В контексте навыка "досье" это особенно чувствительно: даже если передача задумана функционально, она означает экспорт потенциально приватной информации третьей стороне без видимого механизма минимизации, явного согласия или редактирования чувствительных фрагментов.

Content

Scanner excerpt · scripts/verify_facts.py (reported line 86)May include surrounding context.

python
CITE_RE = re.compile(r"\[(\d+)\]")
SENT_SPLIT = re.compile(r"(?<=[.!?;])\s+|\n+")
SONAR_URL = "https://api.perplexity.ai/chat/completions"

# Предела нет и быть не должно: проверяется КАЖДОЕ утверждение. Сначала
# стоял срез на 15 — он молча отсекал хвост, и агент отчитывался «все 15

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

В system-сообщении явно задано "Отвечай на русском", что навязывает конкретный язык независимо от предпочтений пользователя или контекста. По правилу SQP-3 это является нарушением языковой/локальной политики, поскольку в файле нет механизма opt-in или выбора языка.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.