Back to skill

Security audit

Foam Notes

Security checks for vulnerabilities and agentic risk

Overview

This Foam notes skill is mostly coherent, but review is needed because several scripts can modify, rename, or delete files outside the intended Foam workspace.

Install only if you are comfortable with local scripts editing your notes. Set the Foam root explicitly, keep backups, avoid --force and --auto-apply, and do not use this version on untrusted note names or paths until workspace-containment checks are fixed.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/create_note.py:176
Finding
Workspace Escape Allows Arbitrary Markdown File Creation## Vulnerability Details **File Location**: `scripts/create_note.py`, lines 176–183 and 277–278 **Vulnerability Type**: Path traversal and missing workspace containment validation **Risk Level**: Medium ### Vulnerable Code ```python # Determine output directory if output_dir is None: output_dir = foam_root elif not output_dir.is_absolute(): output_dir = foam_root / output_dir output_dir.mkdir(parents=True, exist_ok=True) # Generate filename slug = slugify(title) date_str = datetime.now().strftime("%Y-%m-%d") filename = f"{slug}.md" filepath = output_dir / filename # Check for existing file counter = 1 original_filepath = filepath while filepath.exists(): filename = f"{slug}-{counter}.md" filepath = output_dir / filename counter += 1 # Write the file filepath.write_text(content) print(f"Created: {filepath.relative_to(foam_root)}") ``` ### Technical Analysis The `--dir` argument is documented as relative to the Foam workspace, but the implementation accepts both absolute paths and relative paths containing `..`. Relative paths are joined to `foam_root` without normalization or a subsequent containment check. Absolute paths bypass the workspace join entirely. The script creates the destination directory and writes the file before calling `relative_to(foam_root)`. Consequently, the final display operation may raise an exception for an escaped path, but the external file has already been created. The generated filename is constrained by `slugify()` and receives a `.md` extension. Existing files are not overwritten because a numeric suffix is selected. Nevertheless, an attacker can create Markdown files and parent directories at arbitrary writable locations. ### Attack Path 1. Identify a directory writable by the account executing the Skill. 2. Invoke the script with an absolute or traversal-based output directory, for example: ```bash python3 scripts/create_n ...[truncated 729 chars]
Remediation
## Remediation Suggestions - Reject absolute values for `--dir` because the option is documented as workspace-relative. - Resolve both the workspace and destination before creating directories: ```python root = foam_root.resolve(strict=True) destination = (root / output_dir).resolve() if not destination.is_relative_to(root): raise ValueError("Output directory must remain inside the Foam workspace") ``` - Repeat containment validation on the final file path immediately before writing. - Consider rejecting path components equal to `..` for clearer user-facing validation. - Avoid following symlinks that lead outside the workspace. Validate the resolved destination after its parent exists. - Perform the validation before `mkdir()` so escaped directories are not created.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/daily_note.py:57
Finding
Unvalidated Daily Note Folder Allows Config-Driven Workspace Escape## Vulnerability Details **File Location**: `scripts/daily_note.py`, lines 57–67 and 133–136 **Vulnerability Type**: Absolute-path injection and path traversal through configuration **Risk Level**: Medium ### Vulnerable Code ```python def get_daily_note_path( foam_root: Path, date: datetime, daily_folder: str = "journals" ) -> Path: """Get the path for a daily note.""" # Default: journals/yyyy-mm-dd.md or custom folder from config journals_dir = foam_root / daily_folder if not journals_dir.exists(): journals_dir = foam_root # Fallback to root filename = date.strftime("%Y-%m-%d") + ".md" return journals_dir / filename ``` ```python # Ensure directory exists filepath.parent.mkdir(parents=True, exist_ok=True) # Write file filepath.write_text(content) return True ``` ### Technical Analysis `daily_folder` originates from the `daily_note_folder` configuration setting and is joined directly to `foam_root`. With `pathlib`, joining an absolute right-hand path discards the left-hand path. A value containing `..` may also resolve outside the workspace. If an external absolute directory already exists, the existence check succeeds and the fallback to `foam_root` is not used. The script then writes a date-named Markdown file to that external directory. No resolved-path containment check is performed. ### Attack Path 1. Modify the Skill's `config.json` so `daily_note_folder` points to an existing external directory: ```json { "daily_note_folder": "/tmp/external-notes" } ``` 2. Ensure `/tmp/external-notes` exists. 3. Invoke: ```bash python3 scripts/daily_note.py --foam-root /home/user/foam ``` 4. The absolute configured path supersedes `foam_root`. 5. The script creates a file such as `/tmp/external-notes/2026-09-10.md`. ### Impact Assessment The operation runs with the privileges of the current user. It can create date- ...[truncated 257 chars]
Remediation
## Remediation Suggestions - Require `daily_note_folder` to be a relative path. - Reject absolute paths and any resolved path outside `foam_root`. - Resolve and validate the final daily-note path before checking existence or creating directories: ```python root = foam_root.resolve(strict=True) folder = Path(daily_folder) if folder.is_absolute(): raise ValueError("daily_note_folder must be relative") filepath = (root / folder / filename).resolve() if not filepath.is_relative_to(root): raise ValueError("Daily note path escapes the Foam workspace") ``` - Validate symlink-resolved parents to prevent a workspace symlink from redirecting writes externally. - Report invalid configuration instead of silently falling back to another directory.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/suggest_tags.py:442
Finding
Tag Suggestions Can Modify Arbitrary Files Outside the Workspace## Vulnerability Details **File Location**: `scripts/suggest_tags.py`, lines 346–394 and 442–449 **Vulnerability Type**: Unrestricted file path used for content modification **Risk Level**: Medium ### Vulnerable Code ```python def apply_tags(note_path: Path, tags_to_add: list, mode: str = "inline") -> bool: """Add tags to note.""" if not tags_to_add: return False content = note_path.read_text() # Clean up tags (remove # if present) clean_tags = [t.lstrip("#") for t in tags_to_add] if mode == "frontmatter": # Add to frontmatter if content.startswith("---"): # Has frontmatter end = content.find("---", 3) if end > 0: # Check if tags field exists fm = content[3:end] if "tags:" in fm: mode = "inline" else: new_fm = fm.rstrip() + f"\ntags: [{', '.join(clean_tags)}]\n" content = "---" + new_fm + content[end:] else: fm = f"---\ntags: [{', '.join(clean_tags)}]\n---\n\n" content = fm + content if mode == "inline": if content.startswith("---"): end = content.find("---", 3) if end > 0: insert_pos = end + 3 if not content[insert_pos:].startswith("\n"): insert_pos = content.find("\n", insert_pos) + 1 else: insert_pos = 0 else: insert_pos = 0 tag_line = " ".join(f"#{t}" for t in clean_tags) + "\n\n" content = content[:insert_pos] + tag_line + content[insert_pos:] note_path.write_text(content) return True ``` ```python # Resolve note path note_path = Path(args.note) if not note_path.is_absolute(): note_path = foam_root / note_path if not note_path.exists(): print(f"Er ...[truncated 1793 chars]
Remediation
## Remediation Suggestions - Remove support for absolute note paths unless external editing is an explicit, separately authorized feature. - Resolve the workspace and target, then require the target to be contained within the workspace. - Require `note_path.is_file()` and a `.md` suffix. - Validate the resolved path immediately before reading and again before writing to reduce symlink and race-condition exposure. - Write through a temporary file located in the same validated directory and use an atomic replacement only after successful processing. - Ensure status formatting occurs before mutation or safely handles external paths without masking a completed write.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/suggest_wikilinks.py:442
Finding
Wikilink Suggestions Can Rewrite Arbitrary Files Outside the Workspace## Vulnerability Details **File Location**: `scripts/suggest_wikilinks.py`, lines 354–390 and 442–449 **Vulnerability Type**: Unrestricted file modification and missing path containment **Risk Level**: Medium ### Vulnerable Code ```python def apply_wikilinks( note_path: Path, candidates: list, selections: list, with_aliases: bool = False ) -> bool: """Apply selected wikilinks to the note.""" if not selections: return False content = note_path.read_text() lines = content.split("\n") to_apply = sorted( [(i, candidates[i - 1]) for i in selections if 1 <= i <= len(candidates)], key=lambda x: (-x[1]["line"], -x[1]["column"]), ) for _, candidate in to_apply: line_idx = candidate["line"] - 1 col_idx = candidate["column"] - 1 text = candidate["text"] target = candidate["target"] line = lines[line_idx] before = line[:col_idx] after = line[col_idx + len(text) :] if with_aliases: lines[line_idx] = before + f"[[{target}|{text}]]" + after else: lines[line_idx] = before + f"[[{target}]]" + after # Write back note_path.write_text("\n".join(lines)) return True ``` ```python # Resolve note path note_path = Path(args.note) if not note_path.is_absolute(): note_path = foam_root / note_path if not note_path.exists(): print(f"Error: Note not found: {note_path}", file=sys.stderr) sys.exit(1) ``` ### Technical Analysis The target path can be absolute or can escape through `..`. The only gate is an existence check. There is no workspace-containment check, regular-file validation, extension validation, or symlink-target validation. `--auto-apply` makes exploitation noninteractive: every generated candidate is inserted into the selected external file. The script writes the modified content before attempting to rend ...[truncated 947 chars]
Remediation
## Remediation Suggestions - Limit input to Markdown files contained in `foam_root`. - Resolve and compare paths before scanning or applying suggestions: ```python root = foam_root.resolve(strict=True) target = (root / args.note).resolve() if not Path(args.note).is_absolute() else Path(args.note).resolve() if not target.is_relative_to(root): raise ValueError("Note must be inside the Foam workspace") ``` - Require a regular `.md` file and reject external symlink targets. - Revalidate immediately before `write_text()`. - Keep `--auto-apply` disabled unless the validated target is unambiguously inside the workspace. - Consider atomic writes and backups for automated bulk modifications.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/delete_note.py:27
Finding
Path Traversal Allows Deletion of Markdown Files Outside the Workspace## Vulnerability Details **File Location**: `scripts/delete_note.py`, lines 27–34 and 175–179 **Vulnerability Type**: Path traversal leading to arbitrary file deletion **Risk Level**: High ### Vulnerable Code ```python def find_note(target: str, foam_root: Path) -> Path: """Find a note by name or filename.""" target_stem = target.replace(".md", "") target_slug = slugify(target_stem) # Try exact match first exact_path = foam_root / f"{target_stem}.md" if exact_path.exists(): return exact_path ``` ```python # Delete or backup if backup: backup_path = backup_note(note_path, foam_root) print(f"Backed up to: {backup_path.relative_to(foam_root)}") else: note_path.unlink() print(f"Deleted: {note_path.relative_to(foam_root)}") ``` ### Technical Analysis `target` is treated as a note name, but path separators and `..` components are not prohibited. The exact-path lookup directly appends the attacker-controlled value to `foam_root`. If an external Markdown file exists at the resulting traversal path, it is accepted as the note. When `--force` is supplied, the confirmation block—which would call `relative_to()` and potentially fail for an external path—is skipped. The script reaches `unlink()` and permanently deletes the external file. The final display operation occurs only after deletion. The attack is restricted to paths that become `.md` files because the function removes occurrences of `.md` and appends `.md` again. However, deletion of arbitrary writable Markdown files outside the workspace remains possible. ### Attack Path 1. Assume `/home/user/victim.md` exists and the Foam root is `/home/user/foam`. 2. Invoke: ```bash python3 scripts/delete_note.py ../victim \ --foam-root /home/user/foam \ --force ``` 3. `find_note()` constructs `/home/user/foam/../victim.md`. 4. Because the file exists, it is returned as the selected ...[truncated 631 chars]
Remediation
## Remediation Suggestions - Treat `target` strictly as a note name and reject `/`, `\`, absolute paths, `.` components, and `..` components. - Resolve every candidate returned by `find_note()` and require it to remain under the resolved workspace. - Require the target to be a regular `.md` file and define an explicit policy for symlinks. - Perform containment validation immediately before `unlink()` or `shutil.move()`, not only during initial lookup. - Do not rely on `relative_to()` used for display as a security check. - Consider making backup deletion the default and requiring an explicit option for permanent deletion. - Add regression tests for `../victim`, absolute paths, nested traversal, and external symlinks.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/rename_note.py:22
Finding
Path Traversal Allows Renaming Markdown Files Outside the Workspace## Vulnerability Details **File Location**: `scripts/rename_note.py`, lines 22–27 and 146–154 **Vulnerability Type**: Path traversal leading to unauthorized external file rename **Risk Level**: High ### Vulnerable Code ```python def find_note(target: str, foam_root: Path) -> Path: """Find a note by name or filename.""" target_stem = target.replace(".md", "") # Try exact match first exact_path = foam_root / f"{target_stem}.md" if exact_path.exists(): return exact_path ``` ```python # Update wikilinks in other notes if backlinks: count = update_wikilinks(old_stem, new_stem, foam_root, backlinks) print(f"Updated wikilinks in {count} note(s).") # Rename the file old_path.rename(new_path) print( f"Renamed: {old_path.relative_to(foam_root)} → {new_path.relative_to(foam_root)}" ) ``` ### Technical Analysis The old note name is concatenated directly with `foam_root` and may contain `..` or path separators. An external Markdown file can therefore be selected as the source. The destination is constructed beside that source: ```python old_stem = old_path.stem new_stem = slugify(new_name) new_path = old_path.parent / f"{new_stem}.md" ``` With `--force`, pre-operation calls to `relative_to(foam_root)` are skipped. The external source file is renamed before the final display operation attempts relative-path conversion. The new name is slugified, so the attacker cannot independently choose an arbitrary destination directory. Nevertheless, the attacker can rename a writable external Markdown file within its current directory, effectively removing its original path and disrupting unrelated projects. ### Attack Path 1. Assume `/home/user/victim.md` exists and the Foam root is `/home/user/foam`. 2. Invoke: ```bash python3 scripts/rename_note.py ../victim displaced \ --foam-root /home/user/foam \ --force ``` 3. `find_note()` sele ...[truncated 621 chars]
Remediation
## Remediation Suggestions - Reject path separators, absolute paths, and traversal components in `old_name`. - Resolve `old_path` and require it to be under the resolved Foam root. - Resolve and validate both source and destination immediately before calling `rename()`. - Require both paths to remain in the same authorized workspace. - Reject symlink sources or explicitly validate their resolved targets. - Preserve confirmation as a usability safeguard, but do not treat it as a replacement for path authorization. - Add tests covering `--force` with traversal, absolute source paths, external symlinks, and destination collisions.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (17)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The code chunk is narrowly focused on note creation from templates within a Foam workspace. It does support part of the declared description: creating notes, daily notes, and templates. However, it does not implement editing, linking, tagging operations beyond inserting empty/default tags in frontmatter, intelligent wikilink or tag suggestions, backlinks discovery, graph visualization, deletion, renaming, or documentation querying. Because the declared purpose presents a much broader multifunction Foam skill than the code actually provides, the description does not accurately represent this chunk's behavior.

Ae1

High
Category
analysis-evasion
Content
**Location**: `config.json` in the skill directory (next to `SKILL.md`).
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill documents capabilities that require reading environment variables and reading/writing workspace files, but it does not declare any explicit tool scope or permission boundaries. That creates an authorization ambiguity where an agent may exercise broader file or env access than a reviewer expects, increasing the chance of unintended workspace modification or disclosure of sensitive local data.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
```bash
python3 scripts/delete_note.py "Old Note"                    # Interactive deletion
python3 scripts/delete_note.py "Old Note" --force          # Skip confirmation
python3 scripts/delete_note.py "Old Note" --backup         # Move to .foam/trash/
python3 scripts/delete_note.py "Old Note" --fix-links      # Remove wikilinks from other notes
```
Confidence
85% confidence
Finding
The documented --force option allows destructive deletion to bypass confirmation, which is risky in an agentic context where commands may be constructed or invoked without sufficient human review. If used on the wrong target or workspace, it can cause immediate data loss or loss of important notes.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
**Features:**
- **Backup mode**: Moves note to `.foam/trash/` instead of permanent deletion
- **Backlink detection**: Shows which notes link to the one being deleted
- **Link fixing**: Automatically removes wikilinks from other notes
- **Confirmation**: Prompts before deletion (skip with `--force`)

### rename_note.py
Confidence
80% confidence
Finding
Automatic backlink fixing during deletion modifies other notes without requiring per-change review, which can propagate unintended content changes across the workspace. In an agent setting, this increases the blast radius of a mistaken delete operation from one file to many files.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
```bash
python3 scripts/rename_note.py "Old Name" "New Name"       # Interactive rename
python3 scripts/rename_note.py "Old Name" "New Name" --force  # Skip confirmation
```

**Features:**
Confidence
85% confidence
Finding
A forced rename that skips confirmation can automatically update many wikilinks and rename a file without human validation of the affected references. While less destructive than deletion, it can still corrupt note organization or introduce widespread link errors if the wrong note is targeted.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
except (json.JSONDecodeError, IOError) as e:
            print(
                f"Warning: Could not load config.json: {e}",
                file=__import__("sys").stderr,
            )
            return default_config
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
except (json.JSONDecodeError, IOError) as e:
            print(
                f"Warning: Could not load config.json: {e}",
                file=__import__("sys").stderr,
            )
            return default_config
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Content
with open(config_path, "w") as f:
            json.dump(config, f, indent=2)
    except IOError as e:
        print(f"Error: Could not save config.json: {e}", file=__import__("sys").stderr)


def find_foam_root_auto(start_dir: Path = None) -> Path:
Confidence
75% confidence
Finding
Dynamic __import__() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Examples:
    python3 rename_note.py "My Note" "My Better Note"     # Interactive rename
    python3 rename_note.py "My Note" "My Better Note" --force  # Skip confirmation
"""

import argparse
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Examples:
    python3 rename_note.py "My Note" "My Better Note"     # Interactive rename
    python3 rename_note.py "My Note" "My Better Note" --force  # Skip confirmation
"""

import argparse
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Examples:
    python3 rename_note.py "My Note" "My Better Note"     # Interactive rename
    python3 rename_note.py "My Note" "My Better Note" --force  # Skip confirmation
"""

import argparse
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Examples:
    python3 rename_note.py "My Note" "My Better Note"     # Interactive rename
    python3 rename_note.py "My Note" "My Better Note" --force  # Skip confirmation
"""

import argparse
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Tainted flow: 'content' from pathlib.Path.read_text (line 392, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
tag_line = " ".join(f"#{t}" for t in clean_tags) + "\n\n"
        content = content[:insert_pos] + tag_line + content[insert_pos:]

    note_path.write_text(content)
    return True
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.

Tainted flow: 'lines' from pathlib.Path.read_text (line 362, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
lines[line_idx] = before + f"[[{target}]]" + after

    # Write back
    note_path.write_text("\n".join(lines))
    return True
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.

Context-Inappropriate Capability

Low
Confidence
77% confidence
Finding
The manifest describes working with Foam note repositories by creating, editing, linking, tagging, and suggesting links/tags. This file documents support for executable JavaScript templates that implement arbitrary logic for note creation, which is a broader programmable capability not clearly justified by the stated purpose.

Missing User Warnings

Low
Confidence
91% confidence
Finding
This code file supports a non-interactive --apply mode that ultimately writes modified content back to the note file. Although the script reports changes after writing, it does not provide a user-facing warning at the point the auto-apply option is defined that using this flag will modify the note contents immediately.

Static analysis

No suspicious patterns detected.