Back to skill

Security audit

Portfolio Risk Desk

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent portfolio-briefing skill, but live Apify mode and local state handling can expose credentials or write outside intended state paths.

Review before installing, especially if enabling live providers. Use a narrowly scoped Apify token, keep APIFY_BASE_URL fixed to the real Apify API, avoid sharing generated briefs from failed live runs until token redaction is fixed, and disable or tightly constrain LOCAL_STATE_DIR unless workspace/profile path validation is added. Treat Notion and memory outputs as persistent copies of sensitive portfolio context.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
src/intelligence_desk_brief/retrieval.py:157
Finding

Apify API token is embedded in URLs and can leak into generated brief output

Content
View full analysis

Vulnerability Details

File Location: src/intelligence_desk_brief/retrieval.py:157-169, 181-205; src/intelligence_desk_brief/synthesis.py:338-348; src/intelligence_desk_brief/renderer.py:88-96
Vulnerability Type: Credential exposure through query strings and error output
Risk Level: High

Vulnerable Code

python
query = urlencode(
    {
        "token": self._config.apify_api_token or "",
        "timeout": str(self._config.apify_timeout_seconds),
        "format": "json",
        "clean": "1",
        "limit": str(self._config.apify_max_items),
    }
)
base_url = self._config.apify_base_url.rstrip("/")
url = f"{base_url}/actor-tasks/{task_id}/run-sync-get-dataset-items?{query}"

On failure or an empty response, the credential-bearing URL is retained in a notice:

python
notice = self._build_notice(
    lane=lane,
    url=url,
    fact=f"{lane} retrieval failed after {self._config.apify_retry_attempts} attempts: {last_error}",
    raw_reference={"error": str(last_error), "task_input": task_input},
)
python
notice = self._build_notice(
    lane=lane,
    url=url,
    fact=f"{lane} retrieval returned no usable results.",
    raw_reference={"task_input": task_input, "raw_payload": items[:3]},
    retrieval_status="empty",
)

The URL is subsequently included in synthesized evidence:

python
def _build_evidence_and_sources(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
    ordered = sorted(evidence, key=lambda item: item["ranking"]["score"], reverse=True)
    return [
        {
            "evidence_id": item["id"],
            "source_title": item["source"]["title"],
            "url_or_publisher": item["source"]["url"],
            "timestamp_or_recency_note": item["source"]["timestamp"] or item["source"]["recency_note"],
            "why_it_mattered": f"[{item['source_lane']}] {item['why_it_matters']}",
            "fact_excerpt": item["fact"],
            "interpretation_excerpt": item["interpretation"],
...[truncated 2653 chars]
Remediation
View remediation

Remediation Suggestions

  1. Send the Apify token only through an authorization header:

    python
    request = Request(
        url_without_token,
        data=body,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    
  2. Never store a credential-bearing request URL in evidence, notices, exceptions, logs, or handoff payloads.

  3. Build a sanitized display URL containing only the provider origin and non-sensitive task reference.

  4. Introduce a centralized redaction function that removes known secrets and sensitive query parameters from error messages before logging or returning them.

  5. Avoid placing raw upstream error bodies or request details into user-facing artifacts unless they have been sanitized.

  6. Add regression tests that use a distinctive fake token and assert it does not appear in:

    • Raw retrieval results
    • Failure and empty-result notices
    • Normalized evidence
    • JSON responses
    • Rendered Markdown
    • Structured logs
    • Delivery handoff payloads
  7. Rotate any real token that may already have appeared in generated output or retained infrastructure logs.

T09 · Insecure Skill Coding Practices

Error
Location
src/intelligence_desk_brief/config.py:28
Finding

Configurable provider URL can exfiltrate credentials and portfolio-derived data

Content
View full analysis

Vulnerability Details

File Location: src/intelligence_desk_brief/config.py:28-32, 43-50; src/intelligence_desk_brief/apify_bootstrap.py:51-60, 86-101; src/intelligence_desk_brief/retrieval.py:125-128, 157-181
Vulnerability Type: Unvalidated outbound destination for authenticated requests
Risk Level: High

Vulnerable Code

The outbound provider origin can be supplied through the environment without validation:

python
@dataclass(frozen=True)
class AppConfig:
    apify_api_token: str | None = None
    apify_task_id: str | None = None
    apify_x_task_id: str | None = None
    apify_base_url: str = "https://api.apify.com/v2"
python
config = cls(
    civic_client_id=env.get("CIVIC_CLIENT_ID"),
    apify_api_token=env.get("APIFY_API_TOKEN"),
    apify_task_id=env.get("APIFY_TASK_ID"),
    apify_x_task_id=env.get("APIFY_X_TASK_ID"),
    apify_base_url=env.get("APIFY_BASE_URL", "https://api.apify.com/v2"),

Bootstrap sends the bearer token to that configurable destination:

python
normalized_base_url = base_url.rstrip("/")
existing_tasks = _list_tasks(base_url=normalized_base_url, token=token)
python
url = f"{base_url}{path}"
body = None if payload is None else json.dumps(payload).encode("utf-8")
request = Request(
    url,
    data=body,
    method=method,
    headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    },
)
try:
    with urlopen(request, timeout=60) as response:

Automatic bootstrap uses the same configured origin:

python
created = ensure_apify_tasks(
    token=self._config.apify_api_token,
    base_url=self._config.apify_base_url,
    include_x_signals=self._config.enable_x_signals,
)

Live retrieval also sends a token and portfolio-derived task input to it:

python
query = urlencode(
    {
        "token": self._config.apify_api_token or "",
        "timeout": str(self._config.apify_timeout_seconds),
        "format": "json",
        "clean":
...[truncated 2495 chars]
Remediation
View remediation

Remediation Suggestions

  1. Parse APIFY_BASE_URL with urllib.parse.urlsplit.
  2. Require:
    • Scheme exactly https
    • Hostname exactly api.apify.com
    • No embedded username or password
    • No fragment
    • Only expected API path prefixes
  3. Prefer removing production configurability entirely and use a constant trusted origin.
  4. If custom origins are necessary for development:
    • Require an explicit development-only flag
    • Reject production credentials when that flag is active
    • Use injected mock transports in tests instead of changing the provider origin
    • Display a clear warning that credentials will not be attached
  5. Prevent redirects to a different scheme or origin when authorization information is present.
  6. Enforce outbound network policy at the host or container level so this Skill can contact only approved provider endpoints.
  7. Use narrowly scoped provider tokens with spending and resource limits.
  8. Add tests covering HTTP URLs, lookalike domains, embedded credentials, alternate ports, loopback addresses, link-local addresses, and redirect attempts.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/intelligence_desk_brief/providers/local_state.py:23
Finding

Unsanitized workspace and profile identifiers allow local-state path traversal

Content
View full analysis

Vulnerability Details

File Location: src/intelligence_desk_brief/providers/local_state.py:23-66; src/intelligence_desk_brief/profiles.py:34-45; src/intelligence_desk_brief/contracts.py:136-140
Vulnerability Type: Path traversal and workspace-boundary bypass
Risk Level: High

Vulnerable Code

User-controlled identifiers are used directly as filesystem path components:

python
class LocalWorkspaceStore:
    """File-backed workspace state used outside OpenClaw-managed memory."""

    def __init__(self, base_dir: str | Path):
        self._base_dir = Path(base_dir)

    def load_memory_record(self, workspace_id: str, profile_key: str) -> MemoryRecord | None:
        path = self._memory_record_path(workspace_id, profile_key)
        if not path.exists():
            return None
        payload = json.loads(path.read_text(encoding="utf-8"))
        return MemoryRecord(**payload)

    def save_memory_record(self, record: MemoryRecord) -> None:
        path = self._memory_record_path(record.workspace_id, record.profile_key)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(asdict(record), indent=2, sort_keys=True) + "\n", encoding="utf-8")

    def save_profile(self, profile: SavedProfile) -> None:
        path = self._profile_path(profile.workspace_id, profile.profile_id)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(profile.to_dict(), indent=2, sort_keys=True) + "\n", encoding="utf-8")

    def load_profile(self, workspace_id: str, profile_ref: str) -> SavedProfile | None:
        direct_path = self._profile_path(workspace_id, profile_ref)
        if direct_path.exists():
            return SavedProfile.from_dict(json.loads(direct_path.read_text(encoding="utf-8")))
python
def list_profiles(self, workspace_id: str) -> list[SavedProfile]:
    profiles_dir = self._base_dir / workspace_id / "profiles"
    if not profiles_dir.exists():
        return []

...[truncated 3424 chars]
Remediation
View remediation

Remediation Suggestions

  1. Apply a strict identifier policy to workspace and profile IDs, such as:

    python
    IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")
    
  2. Reject:

    • / and \
    • . and ..
    • Absolute paths
    • Drive-qualified paths
    • Empty identifiers
    • Control characters and null bytes
  3. Canonicalize and verify every path before use:

    python
    base = self._base_dir.resolve()
    candidate = (base / workspace_id / "profiles" / f"{profile_id}.json").resolve()
    if not candidate.is_relative_to(base):
        raise ValueError("Resolved local-state path escapes the configured base directory.")
    
  4. Perform containment checks for read, write, and directory-enumeration operations.

  5. Apply slugification or equivalent validation even when an explicit profile_id is supplied.

  6. Use host-authenticated workspace identifiers rather than trusting request-supplied workspace values in multi-user deployments.

  7. Consider opening files relative to a pre-opened directory descriptor with no-follow behavior where supported, reducing symlink-based escape risks.

  8. Create state files with restrictive permissions because they contain portfolio information.

  9. Add tests for:

    • ../ and nested traversal
    • Absolute POSIX paths
    • Windows drive and UNC paths
    • Separator variants
    • Symlink escapes
    • Cross-workspace reads and writes
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (27)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

Saving, listing, and loading user profiles via local CLI/state mechanisms is inconsistent with a simple analysis-and-handoff skill and introduces unauthorized persistence of portfolio-related data. Because holdings and themes can reveal sensitive financial interests, local profile storage creates confidentiality risks and can also let stale or wrong profiles influence later outputs.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The bootstrap instructions tell users to run a command that will create or update external Apify tasks, but they do not clearly warn that this has side effects in a third-party account. In a hosted agent context, unclear side effects can lead to unintended modification of external resources, confusion over account state, and accidental execution under the wrong account or workspace.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
79% confidence
Finding

The skill declares no explicit tool/permission boundaries despite requiring network access, environment-based secrets, and implying file/shell-capable execution paths. In an agent setting, missing scope declarations can let the runtime grant broader capabilities than users expect, increasing the chance of unintended secret access, filesystem writes, or arbitrary external calls.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill explicitly supports writing portfolio research briefs to Notion, which can persist sensitive financial context such as holdings, watchlists, themes, and scenario questions. Without a user-facing warning, consent step, or data-minimization guidance, users may unknowingly cause sensitive investment information to be stored in a third-party workspace where retention, sharing, and access controls may differ from the host environment.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/intelligence_desk_brief/apify_bootstrap.py (reported line 54)May include surrounding context.

python
apify_api_token: str | None = None
    apify_task_id: str | None = None
    apify_x_task_id: str | None = None
    apify_base_url: str = "https://api.apify.com/v2"
    apify_timeout_seconds: int = 120
    apify_max_items: int = 8
    apify_retry_attempts: int = 2

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/intelligence_desk_brief/config.py (reported line 31)May include surrounding context.

python
apify_api_token: str | None = None
    apify_task_id: str | None = None
    apify_x_task_id: str | None = None
    apify_base_url: str = "https://api.apify.com/v2"
    apify_timeout_seconds: int = 120
    apify_max_items: int = 8
    apify_retry_attempts: int = 2

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · src/intelligence_desk_brief/config.py (reported line 49)May include surrounding context.

python
apify_api_token: str | None = None
    apify_task_id: str | None = None
    apify_x_task_id: str | None = None
    apify_base_url: str = "https://api.apify.com/v2"
    apify_timeout_seconds: int = 120
    apify_max_items: int = 8
    apify_retry_attempts: int = 2

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The orchestrator does more than emit a host-consumable handoff: when the delivery target is Notion it actively invokes write_brief_to_notion, and it also persists briefing history through the memory adapter. That expands the skill's data-handling and external side effects beyond the stated behavior, increasing the risk of unintended data exfiltration or retention of sensitive portfolio, profile, and workspace information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The Notion delivery request includes full brief content plus workspace_id, profile_id, and profile_name, and this file shows no user-visible notice, minimization, or consent enforcement before transmission. In a portfolio-risk skill, that can expose sensitive financial interests and identity-linked metadata to an external service, making privacy and compliance issues more serious.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

Exposing a direct public write_brief_to_notion method creates an additional undocumented capability that allows callers to trigger external content delivery independently of the main brief-generation flow. Undocumented write surfaces are dangerous because they bypass expected policy, consent, or validation assumptions tied to the normal orchestration path.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

This code persists workspace memory records and saved profiles to local JSON files via write_text, which affects user data on disk. While the module docstring notes it is JSON-backed, there is no user-facing prompt, warning, logging, or inline disclosure near the write operations to alert users that their data will be stored locally.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The retrieval inputs hard-code locale settings such as language: "en" and tweetLanguage: "en", which constrains results to English regardless of user preference. This is a natural-language policy concern because the file does not offer opt-in language selection or explain why English-only retrieval is required.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/test_cli_e2e.py (reported line 15)May include surrounding context.

python
class CliE2ETests(unittest.TestCase):
    def _run_cli(self, *args: str) -> subprocess.CompletedProcess[str]:
        return subprocess.run(
            [
                sys.executable,
                "-m",

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/test_saved_profiles.py (reported line 19)May include surrounding context.

python
with tempfile.TemporaryDirectory() as temp_dir:
            env = {"PYTHONPATH": "src"}

            save_result = subprocess.run(
                [
                    sys.executable,
                    "-m",

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/test_saved_profiles.py (reported line 41)May include surrounding context.

python
self.assertEqual(saved_profile["profile_id"], "semis-core")
            self.assertEqual(saved_profile["workspace_id"], "demo-workspace")

            list_result = subprocess.run(
                [
                    sys.executable,
                    "-m",

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/test_saved_profiles.py (reported line 62)May include surrounding context.

python
self.assertEqual(len(listed_profiles), 1)
            self.assertEqual(listed_profiles[0]["profile_name"], "semis-core")

            create_result = subprocess.run(
                [
                    sys.executable,
                    "-m",

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The markdown states that the skill performs automatic Apify task bootstrap from APIFY_API_TOKEN, which implies use of sensitive credentials and outbound provider interaction. The README does not include any user-facing warning about handling the token, external account changes, or network activity associated with this operation.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

This manifest text says the skill can create a brief 'daily or on-demand' for several target types, but it does not define a narrow invocation scope, explicit trigger phrases, or negative examples. In a manifest file, that kind of broad natural-language activation description can increase the chance of unintended invocation overlap with other generic briefing or analysis requests.

Content

No source excerpt is available for this finding.

Unverifiable Dependency: setuptools has 10 known advisory(ies) (CVE-2013-1633 (Setuptools vulnerable to Man-in-the-middle attacks); CVE-2025-47273 (setuptools has a path traversal vulnerability in PackageIndex.download that lead); CVE-2024-6345 (setuptools vulnerable to Command Injection via package URL) +7 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.