Back to skill

Security audit

Reading Widget Clawhub

Security checks for vulnerabilities and agentic risk

Overview

This reading widget appears purpose-built rather than malicious, but it mixes a local background service, agent settings access, and weak localhost controls in ways users should review before installing.

Review this before installing. Prefer providing WEREAD_API_KEY explicitly through a controlled environment or dedicated secret store, avoid putting an api_key field in config.json, and do not enable the LaunchAgent unless you want a long-running background process. The local helper should ideally be hardened to serve only widget.html and require a per-install token for /set-goal.

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
server.py:36
Finding
Runtime Directory Exposure Can Disclose Configuration and API Credentials<![CDATA[ ## Vulnerability Details **File Location**: `server.py:36-39`; related credential-loading behavior at `update.py:9-13` **Vulnerability Type**: Unrestricted static file exposure and insecure secret storage fallback **Risk Level**: Medium ### Vulnerable Code `server.py:36-39`: ```python class Handler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(ROOT), **kwargs) ``` `update.py:9-13`: ```python CONFIG = json.loads((ROOT / "config.json").read_text()) def _load_key(): k = os.environ.get("WEREAD_API_KEY") or CONFIG.get("api_key") if k: return k ``` ### Technical Analysis `SimpleHTTPRequestHandler` is configured to serve the entire runtime directory rather than an explicit allowlist of public files. Consequently, any file beneath `ROOT` may be requested through the loopback HTTP service, including `config.json`, Python and shell source files, generated HTML fragments, and any other files later placed in that directory. This becomes a credential-disclosure vulnerability because `update.py` explicitly supports loading `WEREAD_API_KEY` from the `api_key` field in `config.json`. Although the supplied default configuration does not contain a key and the documentation recommends protected settings files, the implemented fallback permits a sensitive bearer credential to be stored in a file exposed by the HTTP server. Binding to `127.0.0.1` reduces exposure to remote network clients, but it does not enforce filesystem-equivalent access control. Other local processes or users capable of connecting to the loopback port may retrieve files regardless of their filesystem permissions. Browser-based access may also be possible in some scenarios, even if cross-origin response reading is constrained by browser policy. ### Attack Path 1. The user starts `server.py`, directly or through `open-widget.sh`. 2. The helper binds to `127.0.0.1:47900` and serves all files under the runtime directo ...[truncated 972 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace unrestricted `SimpleHTTPRequestHandler` behavior with explicit route handling that serves only `widget.html` and any strictly required public assets. 2. Reject requests for configuration files, source files, logs, hidden files, and generated internal fragments. 3. Remove `CONFIG.get("api_key")` as a credential source. Load the key only from the process environment or a dedicated protected credential store. 4. If file-backed credential storage is unavoidable, place the credential outside the HTTP document root and enforce restrictive filesystem permissions such as mode `0600`. 5. Run the server from a dedicated public subdirectory containing only files intended for browser access. 6. Add automated tests verifying that requests such as `/config.json`, `/update.py`, `/server.py`, and `/helper.log` return `404` or `403`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
server.py:41
Finding
Unauthenticated Localhost Endpoint Is Vulnerable to Cross-Site Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `server.py:41-64`; request construction at `template.html:136` **Vulnerability Type**: Cross-site request forgery against an unauthenticated loopback service **Risk Level**: Medium ### Vulnerable Code `server.py:41-64`: ```python def do_POST(self): if self.path != "/set-goal": self.send_error(404) return try: length = int(self.headers.get("Content-Length", 0)) data = json.loads(self.rfile.read(length) or b"{}") goal = int(data.get("goal", 0)) if not (1 <= goal <= 999): raise ValueError("goal out of range") except Exception: self.send_response(400) self.end_headers() self.wfile.write(b'{"ok":false}') return cfg_path = ROOT / "config.json" cfg = json.loads(cfg_path.read_text()) cfg["goal_hours"] = goal cfg_path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n") regenerate() ``` `template.html:136`: ```html <input class="goal-input" id="goalInput" type="number" min="1" max="999" value="{{GOAL_HOURS}}" oninput="var g=Math.max(1,Math.min(999,parseInt(this.value)||1));var p=Math.min(100,Math.round({{MONTH_HOURS}}/g*100));document.getElementById('goalBar').style.width=p+'%';document.getElementById('goalPct').textContent=p+'%';clearTimeout(window.__goalTimer);window.__goalTimer=setTimeout(function(){fetch('http://127.0.0.1:47900/set-goal',{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify({goal:g})}).catch(function(){});},600);"> ``` ### Technical Analysis The `/set-goal` endpoint changes persistent state without requiring authentication, a per-install token, or a CSRF token. It also does not validate the `Origin`, `Referer`, or `Host` headers. The client deliberately sends JSON data with a `Content-Type` of `text/plain`. This is a CORS-safelisted content type, allowing a ...[truncated 1917 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically random per-install token and require it for all state-changing requests. 2. Validate the `Origin` header against the expected loopback origin and reject missing or unexpected origins. Validate the `Host` header as an additional defense. 3. Require `Content-Type: application/json` and reject `text/plain` requests. This forces cross-origin browser requests through a preflight, although CORS alone must not be treated as authentication. 4. Set an explicit maximum request-body size before reading from `rfile`. 5. Rate-limit `/set-goal` and coalesce regeneration requests so repeated updates cannot continuously launch subprocesses. 6. Consider separating configuration updates from network regeneration and scheduling at most one pending refresh. 7. Return suitable security headers, and avoid exposing the mutation endpoint to browser contexts unless interactive editing is required. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (30)

Agent Config Directory Access

High
Category
Agent Snooping
Content
https://cdn.weread.qq.com/skills/weread-skills.zip
```

解压后看 `SKILL.md` 的「鉴权」一节,按指引申请你自己的 `wrk-xxxxxxxx` key。拿到后写进 `~/.claw/settings.json`(Claude Code 用户为 `~/.claude/settings.json`;**别**写进 shell rc,免得泄露到其它进程):

```json
{
Confidence
90% confidence
Finding
The skill instructs users to place an API key in a broad agent settings file under ~/.claw/settings.json or ~/.claude/settings.json, and elsewhere states that server.py/update.py will read from that file automatically. Accessing shared agent configuration expands the blast radius: any other skill, plugin, or tool with config-directory access may read or misuse the credential, turning this widget into a consumer of high-value shared secrets.

Agent Config Directory Access

High
Category
Agent Snooping
Content
### Step 1 — 确认前置依赖

1. 检查 `WEREAD_API_KEY` 是否已设置:
   - 先看 `~/.claw/settings.json`(Claude Code 用户为 `~/.claude/settings.json`)的 `env` 字段
   - 再看当前 shell 环境变量
2. **如果没有 API key**:
   - 告诉用户 key 格式是 `wrk-xxxxxxxx`,绑定用户身份(vid)
Confidence
97% confidence
Finding
The skill instructs the agent to read from ~/.claw/settings.json or ~/.claude/settings.json, which are agent configuration directories that may contain secrets beyond the intended API key. Accessing high-value config stores is dangerous because a compromised or overly broad skill can exfiltrate or misuse unrelated credentials and settings.

Ae1

High
Category
analysis-evasion
Content
- 让用户参考微信读书官方 skill 包获取:`https://cdn.weread.qq.com/skills/weread-skills.zip`,解压后看 `SKILL.md` 的"鉴权"章节
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Agent Config Directory Access

High
Category
Agent Snooping
Content
chmod +x "$DEST/open-widget.sh"
[ -f "$DEST/config.json" ] || cp "$SRC/config.default.json" "$DEST/config.json"

if [ -z "$WEREAD_API_KEY" ] && ! grep -qs WEREAD_API_KEY "$HOME/.claw/settings.json" "$HOME/.claude/settings.json"; then
  echo ""
  echo "⚠️  WEREAD_API_KEY not set."
  echo "   Apply for one via WeRead Agent Gateway (see SKILL.md)."
Confidence
88% confidence
Finding
This finding is the same sensitive behavior at the same line: the script references ~/.claude/settings.json as part of a grep-based secret presence check. Even without printing file contents, a third-party installer touching agent secret/config stores violates least privilege and creates unnecessary trust in code that should not inspect broader agent state.

Agent Config Directory Access

High
Category
Agent Snooping
Content
chmod +x "$DEST/open-widget.sh"
[ -f "$DEST/config.json" ] || cp "$SRC/config.default.json" "$DEST/config.json"

if [ -z "$WEREAD_API_KEY" ] && ! grep -qs WEREAD_API_KEY "$HOME/.claw/settings.json" "$HOME/.claude/settings.json"; then
  echo ""
  echo "⚠️  WEREAD_API_KEY not set."
  echo "   Apply for one via WeRead Agent Gateway (see SKILL.md)."
Confidence
90% confidence
Finding
This finding is the same sensitive behavior at the same line: the script references ~/.claude/settings.json as part of a grep-based secret presence check. Even without printing file contents, a third-party installer touching agent secret/config stores violates least privilege and creates unnecessary trust in code that should not inspect broader agent state.

Agent Config Directory Access

High
Category
Agent Snooping
Content
echo ""
  echo "⚠️  WEREAD_API_KEY not set."
  echo "   Apply for one via WeRead Agent Gateway (see SKILL.md)."
  echo "   Then add to ~/.claw/settings.json (Claude Code: ~/.claude/settings.json) env, or run:"
  echo "   export WEREAD_API_KEY=wrk-xxxxxxxx"
  echo ""
  exit 0
Confidence
90% confidence
Finding
Skill reads from agent configuration directories (.claude/, .codex/, .gemini/). These directories may contain API keys, personal settings, and other credentials that the skill has no legitimate need to access.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
subprocess.run(
            [sys.executable, str(ROOT / "update.py")],
            cwd=str(ROOT),
            env=os.environ.copy(),
            timeout=60,
            check=False,
        )
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.

Session Persistence

Medium
Category
Rogue Agent
Content
想让数据**一直**在后台每 30 分钟刷新、不用每次手动开卡片,就把 `server.py` 设成开机自启:

```bash
cp com.user.reading-widget.plist ~/Library/LaunchAgents/
# 把里面的 USERNAME 换成你的用户名,/usr/bin/python3 换成 `which python3` 的实际路径
launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist
```
Confidence
86% confidence
Finding
The README instructs users to install a LaunchAgent plist so server.py runs automatically in the background every login. Persistence mechanisms are security-sensitive because they survive restarts and can continue network access, local writes, and secret consumption without ongoing user awareness.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
cp com.user.reading-widget.plist ~/Library/LaunchAgents/
# 把里面的 USERNAME 换成你的用户名,/usr/bin/python3 换成 `which python3` 的实际路径
launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist
```

plist 里**不放** API key —— `server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读。日志在 `~/Desktop/reading-widget/helper.log`。
Confidence
83% confidence
Finding
The README ties the LaunchAgent persistence mechanism to automatic reading of credentials from shared settings, which makes the persisted process more sensitive than a simple local UI helper. A background task that auto-starts and consumes secrets increases exposure if abused or if users forget it remains active.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
cp com.user.reading-widget.plist ~/Library/LaunchAgents/
# 把里面的 USERNAME 换成你的用户名,/usr/bin/python3 换成 `which python3` 的实际路径
launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist
```

plist 里**不放** API key —— `server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读。日志在 `~/Desktop/reading-widget/helper.log`。
Confidence
83% confidence
Finding
The README ties the LaunchAgent persistence mechanism to automatic reading of credentials from shared settings, which makes the persisted process more sensitive than a simple local UI helper. A background task that auto-starts and consumes secrets increases exposure if abused or if users forget it remains active.

Session Persistence

Medium
Category
Rogue Agent
Content
launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist
```

plist 里**不放** API key —— `server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读。日志在 `~/Desktop/reading-widget/helper.log`。

### 另一种摆法:Übersicht
Confidence
81% confidence
Finding
The README continues describing the persisted LaunchAgent and notes log locations, confirming ongoing background execution. Persistent background services are not inherently malicious, but in an agent-skill context they are security-relevant because they can maintain long-lived access to local state and credentials outside the user's immediate session.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill performs sensitive actions including reading agent config, writing files, invoking shell commands, accessing environment variables, and making network requests, yet it declares no explicit tool scope or permission boundaries. This increases the risk of overbroad agent execution because users and the platform cannot easily constrain what the skill is allowed to do.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases are broad enough to match common requests about reading, widgets, or tracking habits, which can cause the skill to activate unexpectedly. Because the skill then performs installation, file copying, local server setup, and possible config access, accidental invocation materially expands risk.

Session Persistence

Medium
Category
Rogue Agent
Content
- `server.py` → 本地小后台(发卡片 + 存目标 + 自动刷新)
   - `open-widget.sh` → 一键打开脚本
   - `config.default.json` → 重命名为 `config.json`
   - `com.user.reading-widget.plist` → launchd 模板(Step 4 用,先别动)
3. 跑一次 `WEREAD_API_KEY=xxx python3 ~/Desktop/reading-widget/update.py` 生成首版 `widget.html`
4. 如果脚本报错(key 无效、网络等),把错误原样给用户看,不要瞎猜原因
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
- `server.py` → 本地小后台(发卡片 + 存目标 + 自动刷新)
   - `open-widget.sh` → 一键打开脚本
   - `config.default.json` → 重命名为 `config.json`
   - `com.user.reading-widget.plist` → launchd 模板(Step 4 用,先别动)
3. 跑一次 `WEREAD_API_KEY=xxx python3 ~/Desktop/reading-widget/update.py` 生成首版 `widget.html`
4. 如果脚本报错(key 无效、网络等),把错误原样给用户看,不要瞎猜原因
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
- `server.py` → 本地小后台(发卡片 + 存目标 + 自动刷新)
   - `open-widget.sh` → 一键打开脚本
   - `config.default.json` → 重命名为 `config.json`
   - `com.user.reading-widget.plist` → launchd 模板(Step 4 用,先别动)
3. 跑一次 `WEREAD_API_KEY=xxx python3 ~/Desktop/reading-widget/update.py` 生成首版 `widget.html`
4. 如果脚本报错(key 无效、网络等),把错误原样给用户看,不要瞎猜原因
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
- `server.py` → 本地小后台(发卡片 + 存目标 + 自动刷新)
   - `open-widget.sh` → 一键打开脚本
   - `config.default.json` → 重命名为 `config.json`
   - `com.user.reading-widget.plist` → launchd 模板(Step 4 用,先别动)
3. 跑一次 `WEREAD_API_KEY=xxx python3 ~/Desktop/reading-widget/update.py` 生成首版 `widget.html`
4. 如果脚本报错(key 无效、网络等),把错误原样给用户看,不要瞎猜原因
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
> ⚠️ 这是给用户电脑加一个常驻 LaunchAgent,属于持久化系统改动。**装之前必须明确问一句用户同不同意**,不要默默 load。

1. 把 skill 目录里的 `com.user.reading-widget.plist` 复制到 `~/Library/LaunchAgents/`,并把里面的 `USERNAME` 换成真实用户名、`/usr/bin/python3` 换成 `which python3` 实际路径。
2. 加载:`launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist`

注意 plist 里**不放** `WEREAD_API_KEY`——`server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读 key,别把 key 落进 plist。日志在 `~/Desktop/reading-widget/helper.log`。
Confidence
78% confidence
Finding
The skill explicitly proposes installing a persistent LaunchAgent that keeps the background service running across sessions. Although it does disclose the behavior and asks for consent, persistence is still a security-relevant capability because it creates long-lived code execution on the user's machine.

Session Persistence

Medium
Category
Rogue Agent
Content
> ⚠️ 这是给用户电脑加一个常驻 LaunchAgent,属于持久化系统改动。**装之前必须明确问一句用户同不同意**,不要默默 load。

1. 把 skill 目录里的 `com.user.reading-widget.plist` 复制到 `~/Library/LaunchAgents/`,并把里面的 `USERNAME` 换成真实用户名、`/usr/bin/python3` 换成 `which python3` 实际路径。
2. 加载:`launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist`

注意 plist 里**不放** `WEREAD_API_KEY`——`server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读 key,别把 key 落进 plist。日志在 `~/Desktop/reading-widget/helper.log`。
Confidence
75% confidence
Finding
The same persistence-enabling step also depends on a plist file copied into LaunchAgents, which creates an auto-start mechanism tied to user sessions. This is not inherently malicious, but it is a real persistence capability that should be treated as sensitive.

Session Persistence

Medium
Category
Rogue Agent
Content
> ⚠️ 这是给用户电脑加一个常驻 LaunchAgent,属于持久化系统改动。**装之前必须明确问一句用户同不同意**,不要默默 load。

1. 把 skill 目录里的 `com.user.reading-widget.plist` 复制到 `~/Library/LaunchAgents/`,并把里面的 `USERNAME` 换成真实用户名、`/usr/bin/python3` 换成 `which python3` 实际路径。
2. 加载:`launchctl load ~/Library/LaunchAgents/com.user.reading-widget.plist`

注意 plist 里**不放** `WEREAD_API_KEY`——`server.py` → `update.py` 会自己从 `~/.claw/settings.json` 的 `env` 读 key,别把 key 落进 plist。日志在 `~/Desktop/reading-widget/helper.log`。
Confidence
75% confidence
Finding
The same persistence-enabling step also depends on a plist file copied into LaunchAgents, which creates an auto-start mechanism tied to user sessions. This is not inherently malicious, but it is a real persistence capability that should be treated as sensitive.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The invocation scenarios include ambiguous phrases like '装这个 widget' and '让我也用上' that do not clearly identify this specific skill. In context, ambiguous activation is risky because the skill can install software artifacts, start local services, and suggest persistence changes on the host.

External Transmission

Medium
Category
Data Exfiltration
Content
PY="$(command -v python3 || echo /usr/bin/python3)"

# Start the helper if it isn't already responding.
if ! curl -s -o /dev/null "http://127.0.0.1:$PORT/widget.html"; then
  "$PY" "$ROOT/server.py" >>"$ROOT/helper.log" 2>&1 &
  for i in $(seq 1 20); do
    curl -s -o /dev/null "http://127.0.0.1:$PORT/widget.html" && break
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def regenerate():
    """Re-run update.py in a fresh process so it picks up the latest config.json."""
    try:
        subprocess.run(
            [sys.executable, str(ROOT / "update.py")],
            cwd=str(ROOT),
            env=os.environ.copy(),
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The document declares lang="zh", and the visible UI text throughout the template is Chinese, indicating a fixed language/locale. There is no indication in this file that the user can opt into this locale or select an alternative language.

External Transmission

Medium
Category
Data Exfiltration
Content
<div class="progress-section">
      <div class="progress-row">
        <div class="progress-title">本月目标 · <input class="goal-input" id="goalInput" type="number" min="1" max="999" value="{{GOAL_HOURS}}" oninput="var g=Math.max(1,Math.min(999,parseInt(this.value)||1));var p=Math.min(100,Math.round({{MONTH_HOURS}}/g*100));document.getElementById('goalBar').style.width=p+'%';document.getElementById('goalPct').textContent=p+'%';clearTimeout(window.__goalTimer);window.__goalTimer=setTimeout(function(){fetch('http://127.0.0.1:47900/set-goal',{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify({goal:g})}).catch(function(){});},600);"> 小时</div>
        <div class="progress-pct" id="goalPct">{{GOAL_PCT}}%</div>
      </div>
      <div class="bar"><div class="bar-fill" id="goalBar" style="width:{{GOAL_PCT}}%"></div></div>
Confidence
84% confidence
Finding
The template performs an HTTP fetch to a localhost endpoint from inline JavaScript, transmitting user-modified data outside the page context. Even though the destination is local rather than remote, it is still external to the document and can reach an unintended service if that port is occupied by another application.

Static analysis

No suspicious patterns detected.