Back to skill

Security audit

Questlog

Security checks for vulnerabilities and agentic risk

Overview

Questlog is a disclosed local commitments ledger and cockpit that stores user-directed local state without hidden execution, exfiltration, or persistence outside its stated purpose.

Install from an inspected or pinned source, set QUESTLOG_ROOT to a dedicated private directory, and treat the loopback UI as single-user local software. Do not expose its port, do not run it as root, and review any future external scheduler, mail, calendar, or action-runner integration separately.

Vulnerability Patterns
  • 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
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (17)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared purpose is a local commitments ledger, but the detected behavior indicates unrelated security testing, HTML/JS interpolation analysis, and Node-based execution of frontend escaper functions. That mismatch is dangerous because it suggests hidden or undeclared functionality, which can be used to execute code, inspect unrelated project files, or pivot into broader system analysis under the cover of a benign productivity skill.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · tests/test_runtime.py (reported line 179)May include surrounding context.

python
children = [subprocess.Popen([sys.executable, str(PACKAGE / "scripts/ledger.py"),
                    "capture", f"synthetic process {i}"], stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE, text=True,
                    env={**os.environ, "QUESTLOG_ROOT": str(self.module.REPO)}) for i in range(6)]
        completed = [(child, child.communicate(timeout=20)) for child in children]
        for child, (output, error) in completed:
            self.assertEqual(child.returncode, 0, output + error)

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
70% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · ui/server.py (reported line 125)May include surrounding context.

python
def git(*args):
    """Explicit local journal only; never remote operations or arbitrary hooks."""
    environment = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
    environment.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1", GIT_OPTIONAL_LOCKS="0")
    return subprocess.run(["git", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false",
                           "-c", "commit.gpgSign=false", "-c", "maintenance.auto=false",

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding

npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 36)May include surrounding context.

md
## Scope and security

The server binds only to loopback. Host and same-origin JSON checks protect the browser surface; request bodies are bounded. This is not a remotely authenticated multi-user service. State is private to the local user, and no transcript, ledger, calendar, account or operational history ships with the package. Runtime initialization never overwrites an existing ledger. Do not use a workspace root as QUESTLOG_ROOT because the dedicated state directory is chmod 0700.

Run `python3 -m unittest discover -s tests -v`. Node.js is required for the HTML escaper behavior tests; no browser/provider account is needed. Synthetic tests cover empty bootstrap, HTTP guards, stale revision rejection, parallel capture and XSS escaping. See [SECURITY.md](SECURITY.md). MIT licensed.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SECURITY.md (reported line 3)May include surrounding context.

md
# Security

This is a local, single-user runtime, not a multi-user service. Never expose its HTTP port to a network, use a reverse proxy without authentication, or run as root. A local process running as you already has access to your state. HTTP Host checks resist DNS rebinding; same-origin JSON mutation checks resist browser CSRF. There is no remote authentication layer.

Keep state and source inputs private. Do not attach real ledgers, transcripts, account caches, machine inventories, tokens, or local paths to public issues. Use synthetic reproductions. Report sensitive vulnerabilities through the repository's private security reporting feature if enabled; otherwise open an issue asking for a private channel without exploit details or private data.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill declares no explicit tool scope even though its behavior implies access to environment data, filesystem reads/writes, networking, and shell execution. In an agent ecosystem, missing scope declarations weakens least-privilege controls and can allow broader-than-expected access when the skill is invoked, increasing the chance of data exposure or unintended command execution.

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_runtime.py (reported line 176)May include surrounding context.

python
def test_cross_process_capture_preserves_history(self):
        self.assertIsNone(self.module.initialize_git())
        children = [subprocess.Popen([sys.executable, str(PACKAGE / "scripts/ledger.py"),
                    "capture", f"synthetic process {i}"], stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE, text=True,
                    env={**os.environ, "QUESTLOG_ROOT": str(self.module.REPO)}) for i in range(6)]

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_xss_escaping.py (reported line 253)May include surrounding context.

python
prog = (stub + js_src + "\n" +
            "const out = %s.map(p => %s(p));\n" % (json.dumps(payloads), fn) +
            "process.stdout.write(JSON.stringify(out));\n")
    r = subprocess.run([node, "-e", prog], capture_output=True, text=True, timeout=30)
    if r.returncode != 0:
        raise AssertionError(f"{fn} failed to execute: {r.stderr.strip()[:300]}")
    return json.loads(r.stdout)

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The module docstring says 'No external actions or adapters enabled' and nearby comments say action execution is excluded from the public runtime, while the code still defines action handling and later exposes it through the HTTP API. Even though the endpoint only enqueues rather than executes actions, the documentation actively conveys that actions are not part of the runtime surface.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The manifest describes a local cockpit for maintaining a commitments ledger, but this file also implements a separate action-enqueueing mechanism that writes instruction records into a queue directory. Creating and exposing pending action records is a broader operational capability than simply maintaining the Markdown ledger and UI state described in the manifest.

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 · ui/server.py (reported line 127)May include surrounding context.

python
"""Explicit local journal only; never remote operations or arbitrary hooks."""
    environment = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
    environment.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1", GIT_OPTIONAL_LOCKS="0")
    return subprocess.run(["git", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false",
                           "-c", "commit.gpgSign=false", "-c", "maintenance.auto=false",
                           "-C", str(REPO), *args],
                          capture_output=True, text=True, timeout=15, env=environment)

Tainted flow: 'REPO' from os.environ.get (line 16, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ui/server.py (reported line 127)May include surrounding context.

python
"""Explicit local journal only; never remote operations or arbitrary hooks."""
    environment = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
    environment.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1", GIT_OPTIONAL_LOCKS="0")
    return subprocess.run(["git", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false",
                           "-c", "commit.gpgSign=false", "-c", "maintenance.auto=false",
                           "-C", str(REPO), *args],
                          capture_output=True, text=True, timeout=15, env=environment)

Tainted flow: 'REPO' from os.environ.get (line 16, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ui/server.py (reported line 161)May include surrounding context.

python
def initialize_git():
    """Opt-in local history; called only by the explicit init --git command."""
    initialize()
    with CAPTURE_LOCK, open(REPO / ".ledger.lock", "a+") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        result = git("init", "--quiet", ".")
        if result.returncode:

Tainted flow: 'REPO' from os.environ.get (line 16, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · ui/server.py (reported line 310)May include surrounding context.

python
def initialize_git():
    """Opt-in local history; called only by the explicit init --git command."""
    initialize()
    with CAPTURE_LOCK, open(REPO / ".ledger.lock", "a+") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        result = git("init", "--quiet", ".")
        if result.returncode:

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The HTML root sets lang="en", which hard-codes English as the interface language. Under the stated policy, locale or language should not be forced unless the skill offers user choice or clearly documents a justified region-specific constraint.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
75% confidence
Finding

The git() docstring asserts the helper is for an explicit local journal only and 'never remote operations,' but the implementation is a generic wrapper over arbitrary git arguments. In this file current call sites are local, yet the helper's documented guarantee is stronger than what the code itself enforces.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
tests/test_runtime.py:71