Back to skill

Security audit

cal-candy: a Markdown calendar

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local Markdown calendar skill with disclosed local file writes and deletions, and no evidence of hidden network access, credential use, or deceptive behavior.

Install only if you want an agent to manage a local Markdown calendar. Confirm before allowing event creation or deletion, keep the calendar directory limited to calendar files, and avoid passing untrusted multiline text as event titles or descriptions.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/mdcal.py:59
Finding

Stored Calendar Event Injection Through Unescaped Event Fields

Content
View full analysis

Vulnerability Details

File Location: scripts/mdcal.py, lines 59-68; attacker-controlled values originate at lines 92-108
Vulnerability Type: Stored Markdown record injection
Risk Level: Medium

Vulnerable Code

python
def write_markdown_events(filepath: Path, events: list):
    """将事件写回 Markdown 文件"""
    lines = ['# 日历\n', '## 事件\n']

    # 按日期和时间排序
    sorted_events = sorted(events, key=lambda e: (e['date'], e['time']))

    for e in sorted_events:
        done = 'x' if e.get('done') else ' '
        title = e.get('title', '无标题')
        desc = f" :: {e['desc']}" if e.get('desc') else ''
        eid = f" #{e['id']}" if e.get('id') else ''
        line = f"- [{done}] {e['date']} {e['time']} {title}{desc}{eid}\n"
        lines.append(line)

    filepath.write_text(''.join(lines), encoding='utf-8')

The untrusted values are accepted and stored without validation:

python
events.append({
    'date': date,
    'time': time,
    'title': title,
    'desc': desc,
    'done': False,
    'id': id,
})

Technical Analysis

The application uses a line-oriented Markdown format as its event database. The event title and description are inserted directly into that format without rejecting or escaping carriage returns, newline characters, or reserved record delimiters.

An attacker who can control a title or description can include a newline followed by another syntactically valid event record. When the file is subsequently read, parse_markdown_events() processes the injected line as an independent event. This is a stored injection issue because the malicious record persists in the monthly Markdown file and affects later commands.

The generated event identifier protects only the legitimate record. It does not prevent injected content from supplying a separate attacker-selected five-character identifier matching the parser's #(\w{5}) requirement.

Attack Path

...[truncated 1239 chars]

Remediation
View remediation

Remediation Suggestions

  • Reject \r and \n in all fields stored in the line-oriented Markdown format.
  • Validate the title and description before modifying any calendar file.
  • Define maximum field lengths to limit malformed or excessively large records.
  • Escape or reject reserved syntax such as :: and trailing identifier patterns when these delimiters can alter parsing behavior.
  • Prefer a structured storage format such as JSON with schema validation, generating Markdown only as a display format.
  • When parsing existing files, verify that dates belong to the expected monthly file and that identifiers satisfy the exact intended format.
  • Add tests covering embedded newlines, carriage returns, delimiter sequences, attacker-selected identifiers, and malformed records.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/mdcal.py:224
Finding

Malformed Markdown Filenames Cause Calendar Command Denial of Service

Content
View full analysis

Vulnerability Details

File Location: scripts/mdcal.py, lines 224-229; equivalent unsafe parsing also occurs at lines 144-150, 263-268, and 382-387
Vulnerability Type: Unvalidated filename parsing
Risk Level: Low

Vulnerable Code

python
for filepath in sorted(files):
    year = int(filepath.stem.split('-')[0])
    month = int(filepath.stem.split('-')[1])
    content = filepath.read_text(encoding='utf-8')
    events.extend(parse_markdown_events(content, year, month))

Equivalent assumptions are made while checking reminders, deleting events, and listing upcoming events:

python
for filepath in sorted(CALENDAR_DIR.glob('*.md')):
    year = int(filepath.stem.split('-')[0])
    month = int(filepath.stem.split('-')[1])

Technical Analysis

The program enumerates every *.md file in the calendar directory and assumes that each filename has the exact YYYY-MM.md structure. It splits the filename and converts components to integers before validating the filename.

A filename such as README.md has no second hyphen-separated component and causes an uncaught IndexError. A filename such as notes-file.md reaches int("notes") and causes an uncaught ValueError. In check_reminders(), the attempted filename validation occurs only after these unsafe conversions and therefore cannot prevent the exception.

Because these exceptions are not handled, one unrelated or deliberately malformed Markdown file can terminate commands that scan the directory.

Attack Path

  1. The attacker or another local process obtains write access to the configured MDCAL_DIR.
  2. A malformed Markdown filename such as README.md or notes-file.md is created in that directory.
  3. The user invokes an affected operation such as list, delete, upcoming, or check.
  4. The command includes the malformed file through glob('*.md').
  5. Filename splitting or integer conversion raises an uncaught e ...[truncated 587 chars]
Remediation
View remediation

Remediation Suggestions

  • Validate each filename before splitting it or converting components to integers.
  • Use an anchored expression such as:
    python
    match = re.fullmatch(r'(\d{4})-(0[1-9]|1[0-2])\.md', filepath.name)
    if not match:
        continue
    year, month = map(int, match.groups())
    
  • Centralize calendar-file discovery in one helper and use it in listing, deletion, upcoming-event, and reminder functions.
  • Skip unrelated Markdown files safely rather than terminating the command.
  • Catch expected filesystem and parsing exceptions and return a controlled diagnostic.
  • Add tests for filenames without hyphens, nonnumeric components, extra components, invalid month values, directories ending in .md, and inaccessible files.
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The trigger description is broad enough to activate on generic conversations about schedules, reminders, or time-bound tasks, which can cause the agent to invoke this skill unexpectedly. Because the skill can add, modify, and delete local calendar data, unintended activation creates a real risk of undesired file changes or privacy-impacting reads of personal schedule information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The documentation describes deletion functionality without clearly warning that it modifies persistent local files under the calendar directory. This can mislead users or calling agents into treating the action as a harmless query, increasing the chance of accidental data loss when the skill deletes calendar entries from local storage.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

This code file contains natural-language strings that effectively force a specific language/locale for users, including the module description and subsequent CLI help/output text. Under the policy, language constraints should either be optional for the user or clearly justified as region-specific; neither is present here.

Content

No source excerpt is available for this finding.

Tainted flow: 'REMIND_FILE' from os.environ.get (line 22, 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 · scripts/mdcal.py (reported line 130)May include surrounding context.

python
reminders[event_key] = minutes

    with open(REMIND_FILE, 'w', encoding='utf-8') as f:
        json.dump(reminders, f, ensure_ascii=False, indent=2)

    print(f"✓ 已设置提醒: 提前 {minutes} 分钟")

Tainted flow: 'REMIND_FILE' from os.environ.get (line 22, 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 · scripts/mdcal.py (reported line 209)May include surrounding context.

python
reminders[event_key] = minutes

    with open(REMIND_FILE, 'w', encoding='utf-8') as f:
        json.dump(reminders, f, ensure_ascii=False, indent=2)

    print(f"✓ 已设置提醒: 提前 {minutes} 分钟")

Tainted flow: 'REMIND_FILE' from os.environ.get (line 22, 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 · scripts/mdcal.py (reported line 305)May include surrounding context.

python
reminders[event_key] = minutes

    with open(REMIND_FILE, 'w', encoding='utf-8') as f:
        json.dump(reminders, f, ensure_ascii=False, indent=2)

    print(f"✓ 已设置提醒: 提前 {minutes} 分钟")

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The file presents user-facing documentation in mixed Chinese and English, but does not state a language preference option or that the skill is intended for a specific locale. This can be a language policy issue when a skill implicitly forces a language presentation without user opt-in.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.