Back to skill

Security audit

Obsidian FNS

Security checks for vulnerabilities and agentic risk

Overview

This skill is purpose-built for remote Obsidian note access, but it handles credentials and vault data in ways users should review carefully before installing.

Install only if you trust the Fast Note Sync server and understand that the skill can read and modify remote vault notes. Use an HTTPS endpoint you control, avoid saving passwords in the config file, protect ~/.config/fast-note-sync/config.json, and treat terminal logs from login as sensitive because they may contain the bearer token.

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/fns.py:91
Finding
Sensitive credentials and vault data can be transmitted over an insecure or attacker-controlled endpoint<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fns.py`, lines 91–109 and 162–171 **Vulnerability Type**: Missing transport security and destination validation **Risk Level**: High ### Vulnerable Code ```python def build_url(base_url: str, path: str, query: Optional[Dict[str, Any]] = None) -> str: base = base_url.rstrip('/') if not path.startswith('/'): path = '/' + path url = base + path if query: clean = {k: v for k, v in query.items() if v is not None and v != ''} if clean: url += '?' + urllib.parse.urlencode(clean) return url def request_json(method: str, url: str, *, token: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, timeout: int = DEFAULT_TIMEOUT, accept_non_json: bool = False) -> Any: data = None headers = {} if payload is not None: data = json.dumps(payload, ensure_ascii=False).encode('utf-8') headers['Content-Type'] = 'application/json' if token: headers['Authorization'] = token if token.lower().startswith('bearer ') else f'Bearer {token}' req = urllib.request.Request(url=url, data=data, method=method.upper(), headers=headers) ``` The login operation passes credentials and a password to the same unrestricted destination: ```python def cmd_login(args: argparse.Namespace) -> None: cfg = effective_config(args) require(cfg, 'baseUrl', 'credentials', 'password') data = request_json( 'POST', build_url(cfg['baseUrl'], '/user/login'), payload={'credentials': cfg['credentials'], 'password': cfg['password']}, timeout=cfg['timeoutSeconds'], ) ``` ### Technical Analysis The remote network behavior is necessary for the declared Fast Note Sync functionality. However, the implementation accepts an arbitrary `baseUrl` from command-line arguments, environment variables, or configuration files without validating its scheme or destination. Consequently, the client permits plain HTTP ...[truncated 1855 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `baseUrl` with `urllib.parse.urlsplit` and reject every scheme other than `https`. 2. Reject URLs containing unexpected user-information components, fragments, malformed hosts, or ambiguous encodings. 3. Permit plain HTTP only through an explicit development-only option such as `--allow-insecure-http`, accompanied by a prominent warning. Consider limiting that exception to loopback addresses. 4. Display the normalized destination host before the initial login and require explicit approval when it changes. 5. Consider supporting an administrator-defined hostname allowlist or certificate pinning for managed deployments. 6. Prevent redirects from forwarding authorization headers or sensitive request bodies to another origin. 7. Document that the endpoint receives authentication data and vault content and must be trusted. 8. Add tests confirming that HTTP, unsupported schemes, malformed URLs, and cross-origin redirects are rejected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fns.py:71
Finding
Authentication secrets are persisted in a plaintext configuration file without enforced restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fns.py`, lines 71–75, 177, and 285–294 **Vulnerability Type**: Insecure local storage of authentication secrets **Risk Level**: Medium ### Vulnerable Code ```python def save_local_config(updates: Dict[str, Any]) -> None: data = get_local_config() data.update({k: v for k, v in updates.items() if v is not None and v != ''}) LOCAL_CONFIG.parent.mkdir(parents=True, exist_ok=True) LOCAL_CONFIG.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') ``` Login automatically persists the returned bearer token: ```python save_local_config({'token': token, 'baseUrl': cfg['baseUrl'], 'credentials': cfg['credentials'], 'vault': cfg.get('vault')}) ``` The configuration command can also persist both a password and token: ```python def cmd_set_config(args: argparse.Namespace) -> None: updates = { 'baseUrl': args.base_url, 'credentials': args.credentials, 'password': args.password, 'vault': args.vault, 'token': args.token, 'timeoutSeconds': args.timeout_seconds, } save_local_config(updates) ``` ### Technical Analysis The Skill stores authentication material in `~/.config/fast-note-sync/config.json` as unencrypted JSON. It does not explicitly create the file with mode `0600`, validate its owner, repair permissions on an existing file, or use an operating-system credential store. The effective permissions therefore depend on the process umask and any permissions already assigned to the file. A permissive umask or pre-created configuration file may make the secrets readable by other local users or processes. Persisting the account password is particularly unnecessary after token-based authentication is available. Although local token reuse is part of the declared functionality, plaintext password storage and the lack of enforced access controls are not required to implement that functionality securely. ### ...[truncated 1079 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store passwords and bearer tokens in the platform's credential manager or keychain rather than in JSON. 2. Do not persist the account password after login; retain only the minimum token required for subsequent operations. 3. If file-based storage remains necessary, create the file atomically with mode `0600` and create its directory with mode `0700`. 4. Before reading or updating the file, verify that it is owned by the current user, is a regular file rather than a symbolic link, and is not accessible by group or other users. 5. Repair unsafe permissions with `os.chmod`, or fail closed and instruct the user to correct them. 6. Use a temporary file in the same protected directory, set its mode before writing secrets, flush it, and atomically replace the destination. 7. Provide a command to remove stored secrets and document their location and security sensitivity. 8. Prefer short-lived, narrowly scoped tokens and support token revocation or rotation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fns.py:178
Finding
Login exposes the complete bearer token through standard output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fns.py`, line 178 **Vulnerability Type**: Sensitive authentication token disclosure **Risk Level**: Medium ### Vulnerable Code ```python print(token if args.raw else json.dumps({'ok': True, 'token': token}, ensure_ascii=False, indent=2)) ``` ### Technical Analysis After successful authentication, the complete bearer token is printed to standard output regardless of whether `--raw` is selected. Without `--raw`, the token is merely wrapped in JSON and remains fully exposed. Standard output is commonly retained in terminal scrollback, agent conversation transcripts, automation logs, CI logs, shell wrappers, monitoring systems, or captured subprocess results. The high-level action runner itself uses `capture_output=True`, demonstrating that subprocess output is expected to be captured in this project. Returning a reusable secret by default is unnecessary because the token has already been saved for later operations. It increases the number of locations containing the token and makes accidental disclosure substantially more likely. ### Attack Path 1. The user or an automation system invokes `python3 scripts/fns.py login`. 2. The server returns a bearer token. 3. The Skill prints the entire token to standard output. 4. A terminal recorder, agent transcript, CI system, subprocess wrapper, or logging service retains that output. 5. An attacker with access to the retained output extracts the token. 6. The attacker submits the token to the configured Fast Note Sync API and acts with the victim's remote authorization. ### Impact Assessment The attacker can obtain the remote privileges represented by the bearer token. Based on the implemented commands, those privileges may include retrieving user information, listing vaults, searching and reading notes, writing or replacing content, renaming or moving notes, inspecting history, and restoring prior versions. The scope and duration depend on server-side ...[truncated 37 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Return only a status object by default, such as `{"ok": true}`, without including the token. 2. Remove `--raw` unless a concrete integration requires it. 3. If explicit token output must remain supported, require a clearly named option such as `--show-token` and print a warning that the output must not be logged. 4. Prefer placing the token directly into a protected credential store rather than passing it through process output. 5. Redact tokens from error reports, diagnostics, telemetry, and agent transcripts. 6. Use short-lived tokens with narrow vault scopes and provide server-side rotation and revocation. 7. Add regression tests verifying that ordinary login output never contains the returned token. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises and operationally relies on shell, network, file read/write, and environment access, but it does not declare any explicit tool scope such as permissions or allowed-tools. That omission weakens least-privilege boundaries and makes it harder for a caller or enforcement layer to restrict the skill to only the minimum capabilities required, increasing the chance of unintended command execution or broader data access in a remote-vault workflow.

Session Persistence

Medium
Category
Rogue Agent
Content
python3 {baseDir}/scripts/fns_actions.py read-note --path "OpenClaw/API-Test.md"
```

### Write note

```bash
python3 {baseDir}/scripts/fns_actions.py write-note \
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.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The usage guide documents low-level operations beyond the stated read/search/write/append scope, including broader file-management commands. This creates a capability mismatch that can mislead operators, bypass least-privilege expectations, and enable destructive actions through a skill that appears narrower than it really is.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The 'Verified capabilities' section explicitly advertises destructive and metadata operations such as prepend, replace, rename, and move that are not reflected in the skill's higher-level description. In a remote vault context, hidden breadth of capability increases the risk of unauthorized modification, data loss, or operator misuse because users may trust the narrower manifest instead of the deeper documentation.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The code persists sensitive values from configuration updates directly to ~/.config/fast-note-sync/config.json, including password and token fields, without enforcing restrictive file permissions or warning the user. On multi-user systems, in misconfigured home directories, backups, or endpoint collection tooling, this can expose reusable credentials and API tokens beyond the intended scope.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The login command sends credentials and password in an HTTP POST request to the configured base URL. While network transmission is central to the tool's purpose, this file provides no visible disclosure, prompt, or comment warning users that sensitive authentication data will be sent to a remote service.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
After login, the returned authentication token is automatically written to local disk for reuse. Persisted bearer tokens are often sufficient for account access without re-entering a password, so compromise of the local config file can directly enable unauthorized access to remote Obsidian vault contents.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The set-config command allows password and token values to be saved to disk in plaintext configuration, again without warning, confirmation, or permission hardening. This broadens the exposure path because secrets may be manually persisted even outside the login flow, increasing risk of leakage through local compromise, backups, logs, or forensic collection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_fns(args):
    cmd = ['python3', str(FNS)] + args
    p = subprocess.run(cmd, capture_output=True, text=True)
    if p.returncode != 0:
        sys.stderr.write(p.stderr or p.stdout)
        raise SystemExit(p.returncode)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
87% confidence
Finding
This markdown file includes commands for `write-note` and `append-note`, which change user data in the vault, but it does not include any caution about overwriting or altering existing notes. For markdown files, SQP-2 applies when descriptions omit warnings about behaviors that could affect user data or system integrity.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The low-level CLI section shows direct modification commands such as `put` and `append`, and the document later lists additional mutating capabilities like `replace`, `rename`, and `move`, but no warning is provided about their impact on stored notes. In a markdown skill reference, omitting such warnings can leave users unaware that these commands alter persisted data.

Static analysis

No suspicious patterns detected.