Back to skill

Security audit

Vikunja

Security checks for vulnerabilities and agentic risk

Overview

This Vikunja skill is purpose-aligned, but it needs review because it handles credentials and tokens unsafely and can delete task/project data without built-in confirmation.

Review before installing. Use only with an HTTPS Vikunja URL, prefer a limited-permission Vikunja account, avoid running the login command where stdout is logged, protect or remove the cached token file, and require explicit confirmation before task or project deletion.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vikunja.py:58
Finding

Vikunja Credentials and Bearer Tokens May Be Transmitted over Plaintext HTTP

Content
View full analysis

Vulnerability Details

File Location: scripts/vikunja.py:58-61; insecure configuration example at SKILL.md:34-40
Vulnerability Type: Cleartext transmission of sensitive information
Risk Level: High

Vulnerable code:

python
import requests

url = f"{cfg['base_url']}/api/v1/login"
resp = requests.post(url, json={"username": cfg["username"], "password": cfg["password"]})

Insecure documented configuration:

yaml
base_url: "http://192.168.1.230:3456"
username: "admin"
password: "admin123"

Technical Analysis

Authentication against the user-configured Vikunja instance is necessary for the Skill's declared functionality. The destination is not hardcoded to an unrelated third party, so this behavior is not evidence of intentional credential exfiltration.

However, the script accepts an http:// base URL without rejection or warning, and the primary configuration example explicitly demonstrates plaintext HTTP. When HTTP is used, the login request sends the username and password without transport encryption. Subsequent API requests also expose bearer tokens, task contents, and modification or deletion requests to network interception and tampering.

The requests library validates certificates when HTTPS is used, but the code does not require HTTPS or distinguish a secure production configuration from an explicit local-development exception.

Attack Path

  1. A user follows the documented example and configures an http:// Vikunja URL.
  2. The user or Agent invokes any command requiring authentication.
  3. The script sends the username and password in a JSON body over plaintext HTTP.
  4. An attacker positioned on the same network, a compromised router, or another network intermediary captures or alters the request.
  5. The attacker authenticates with the captured credentials or replays a captured bearer token.
  6. The attacker reads, creates, updates, completes, labels, or ...[truncated 593 chars]
Remediation
View remediation

Remediation Suggestions

  1. Reject base URLs using plaintext HTTP by default:
    python
    from urllib.parse import urlparse
    
    parsed = urlparse(cfg["base_url"])
    if parsed.scheme != "https":
        raise ValueError("VIKUNJA_BASE_URL must use HTTPS")
    
  2. If local development must support HTTP, require an explicit option such as VIKUNJA_ALLOW_INSECURE_HTTP=1 and display a prominent warning.
  3. Limit any exception to loopback addresses where practical; do not silently allow HTTP for arbitrary LAN or Internet hosts.
  4. Replace the documented HTTP example with an HTTPS URL.
  5. Replace realistic credentials such as admin and admin123 with unmistakable placeholders.
  6. Document trusted TLS certificate deployment for self-hosted instances. Do not recommend disabling certificate verification.
  7. Consider validating that the configured URL contains no embedded user information and normalizing it before endpoint construction.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/vikunja.py:67
Finding

Cached Bearer Token Is Written without Explicitly Restrictive File Permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/vikunja.py:67-69
Vulnerability Type: Insecure local storage of authentication material
Risk Level: Medium

Vulnerable code:

python
token = resp.json()["token"]
TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
TOKEN_PATH.write_text(json.dumps({"token": token, "timestamp": time.time()}))
return token

Technical Analysis

The script persists a reusable bearer token in plaintext at ~/.local/state/vikunja.token, or at a path selected through VIKUNJA_TOKEN_FILE. The file and its parent directory are created using permissions derived from the process umask; the code does not explicitly enforce mode 0600 on the file or mode 0700 on the directory.

On a system with a permissive umask, or when an existing token file has overly broad permissions, other local users may be able to read the bearer token. The script also does not validate ownership, reject symbolic links, or use an atomic secure-file creation procedure before replacing the token file.

Token caching is reasonable for the declared functionality, but storing the credential without explicit access controls exceeds minimum safe secret-handling practices.

Attack Path

  1. The victim runs the helper and successfully authenticates.
  2. The script writes the bearer token to TOKEN_PATH.
  3. A permissive umask, pre-existing file permissions, or an unsafe environment-selected path makes the file readable by another local account.
  4. A local attacker reads and copies the token.
  5. The attacker sends requests to the configured Vikunja instance using Authorization: Bearer <token>.
  6. The attacker acts with the compromised account's Vikunja permissions until the token expires or is invalidated.

A related symlink scenario may be possible when an attacker can control the selected token path or its containing directory, although exploitation depends on local filesystem permissions an ...[truncated 544 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create the state directory with mode 0700.
  2. Create the token file atomically with mode 0600, rather than relying on the process umask.
  3. After writing, explicitly verify the file's owner and permission bits.
  4. Reject symbolic links and non-regular files before reading or replacing the token file.
  5. Write to a securely created temporary file in the same protected directory and atomically replace the final file.
  6. Warn or fail when an existing token file is group-readable or world-readable.
  7. Prefer an operating-system credential store or keyring when available.
  8. Restrict or validate VIKUNJA_TOKEN_FILE so an untrusted environment cannot redirect secrets to an unsafe location.
  9. Add a logout or token-revocation workflow and securely remove the cached token when it is no longer needed.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/vikunja.py:202
Finding

Login Command Unnecessarily Prints the Bearer Token to Standard Output

Content
View full analysis

Vulnerability Details

File Location: scripts/vikunja.py:202-204; usage documented at SKILL.md:22-25
Vulnerability Type: Sensitive token exposure through process output
Risk Level: Low

Vulnerable code:

python
def cmd_login(args, cfg):
    token = get_token(cfg)
    print(token)

Associated documented workflow:

bash
# Authenticate once, then use the token for all requests
TOKEN=$(python3 "$(skill-dir)/scripts/vikunja.py" login)

Technical Analysis

The login command prints a bearer token directly to standard output. This is unnecessary for normal helper operation because get_token() already persists the token and all API commands automatically retrieve it.

Standard output may be retained in Agent conversation records, terminal capture systems, CI logs, shell debugging output, process wrappers, or other operational telemetry. Although command substitution avoids placing the resulting token directly in the shell command line, it does not prevent the helper's output from being captured by the invoking environment.

Attack Path

  1. A user or Agent invokes the documented login command.
  2. The helper prints the bearer token to standard output.
  3. The execution environment, transcript system, CI service, terminal logger, or wrapper records that output.
  4. A person or service with access to the retained output obtains the token.
  5. The token is replayed against the configured Vikunja API before it expires or is revoked.
  6. The attacker performs operations permitted to the compromised account.

Impact Assessment

Exposure permits account impersonation at the Vikunja API level. The accessible scope is determined by the server-side privileges of the authenticated account and may include reading task contents and schedules, modifying or completing tasks, creating resources, and deleting tasks or projects.

Exploitation depends on another party obtaining capture ...[truncated 85 chars]

Remediation
View remediation

Remediation Suggestions

  1. Make login print only a non-sensitive success message, such as Authentication successful.
  2. Rely on the protected token cache for ordinary API operations.
  3. If raw-token output is genuinely required, place it behind an explicit option such as --print-token.
  4. Display a warning explaining that the token grants account access and must not be logged or shared.
  5. Avoid recommending shell variables as the normal authentication workflow when the helper already manages authentication internally.
  6. Update SKILL.md so examples invoke task operations directly and allow automatic authentication.
  7. Ensure automated tests, Agent transcripts, and CI systems redact bearer-token patterns from captured output.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (20)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill invokes a Python helper, reads configuration files, uses environment variables, writes logs, and communicates with a remote REST API, yet the manifest declares no explicit tool or permission scope. That creates an under-specified trust boundary: an agent may grant broader file, environment, and network access than a user expects, increasing the chance of unintended secret exposure or destructive remote actions.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The trigger set includes very common phrases like task, reminder, schedule, project, and inbox, which can match ordinary conversation unrelated to Vikunja. Over-broad activation can cause the agent to invoke this skill unexpectedly, leading to unintended API calls, task creation, updates, or disclosure of task data in contexts where the user did not mean to operate on Vikunja.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 24)May include surrounding context.

Authenticate once, then use the token for all requests

TOKEN=$(python3 "$(skill-dir)/scripts/vikunja.py" login)

Add a task to the Inbox project (id=1)

python3 "$(skill-dir)/scripts/vikunja.py" task create "Walk the dog" --project 1 --due "2026-05-20T18:00:00Z" --priority 2

text

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 25)May include surrounding context.

TOKEN=$(python3 "$(skill-dir)/scripts/vikunja.py" login)

Add a task to the Inbox project (id=1)

python3 "$(skill-dir)/scripts/vikunja.py" task create "Walk the dog" --project 1 --due "2026-05-20T18:00:00Z" --priority 2

text

## Configuration

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill advertises destructive delete operations for tasks and projects but provides no warning, confirmation step, or recovery guidance. In an agent setting, this can turn ambiguous or mistaken user input into irreversible remote deletion of productivity data, especially because project deletion may affect many tasks at once.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% 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 · scripts/vikunja.py (reported line 61)May include surrounding context.

python
import requests

    url = f"{cfg['base_url']}/api/v1/login"
    resp = requests.post(url, json={"username": cfg["username"], "password": cfg["password"]})
    if resp.status_code != 200:
        print(f"Login failed: {resp.status_code} {resp.text}", file=sys.stderr)
        sys.exit(1)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The script persists the authentication token to disk automatically without notifying the user and without setting restrictive file permissions. In multi-user or agent-run environments, this can leave reusable credentials accessible through the filesystem, backups, or redirected paths.

Content

No source excerpt is available for this finding.

Tainted flow: 'TOKEN_PATH' from os.environ.get (line 19, credential/environment) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Confidence
91% confidence
Finding

The token cache path is derived from an environment variable and the script writes the authentication token to that path without validation or permission hardening. In an agent setting, a caller that can influence environment variables could redirect the token into an attacker-controlled location, causing credential disclosure or overwriting files accessible to the process.

Content

Scanner excerpt · scripts/vikunja.py (reported line 68)May include surrounding context.

python
token = resp.json()["token"]
    TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
    TOKEN_PATH.write_text(json.dumps({"token": token, "timestamp": time.time()}))
    return token

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.get (network output)

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 · scripts/vikunja.py (reported line 83)May include surrounding context.

python
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.get (network output)

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 · scripts/vikunja.py (reported line 99)May include surrounding context.

python
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% 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 · scripts/vikunja.py (reported line 85)May include surrounding context.

python
if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% 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 · scripts/vikunja.py (reported line 101)May include surrounding context.

python
if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.put (network output)

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 · scripts/vikunja.py (reported line 85)May include surrounding context.

python
if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.put (network output)

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 · scripts/vikunja.py (reported line 101)May include surrounding context.

python
if method == "GET":
        resp = requests.get(url, headers=headers, params=body if isinstance(body, dict) else None)
    elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% 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 · scripts/vikunja.py (reported line 87)May include surrounding context.

python
elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":
        resp = requests.delete(url, headers=headers)
    else:

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% 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 · scripts/vikunja.py (reported line 103)May include surrounding context.

python
elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":
        resp = requests.delete(url, headers=headers)
    else:

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.post (network output)

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 · scripts/vikunja.py (reported line 87)May include surrounding context.

python
elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":
        resp = requests.delete(url, headers=headers)
    else:

Tainted flow: 'headers' from requests.post (line 80, network input) → requests.post (network output)

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 · scripts/vikunja.py (reported line 103)May include surrounding context.

python
elif method == "PUT":
        resp = requests.put(url, headers=headers, json=body)
    elif method == "POST":
        resp = requests.post(url, headers=headers, json=body)
    elif method == "DELETE":
        resp = requests.delete(url, headers=headers)
    else:

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The login command prints the raw authentication token to stdout, which can expose it to calling agents, logs, shell history, process capture, or other downstream tooling. In an agent skill context this is more dangerous because stdout is commonly collected and relayed, turning a task-management helper into a credential disclosure primitive.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

Printing the authentication token to stdout without warning exposes a reusable bearer credential to any consumer of command output. Because this skill is designed for automation, stdout may be logged or surfaced back to users, substantially increasing leakage risk.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.env_credential_access

Python code POSTs credential environment variables to an environment-controlled URL.

Critical
Code
suspicious.env_credential_access
Location
scripts/vikunja.py:61