Back to skill

Security audit

Grocery Shopping Assistant

Security checks for vulnerabilities and agentic risk

Overview

The grocery checklist purpose is mostly coherent, but the package includes under-scoped Telegram state changes and an unrelated session-pruning utility that is overbroad for this skill.

Install only if you are comfortable with a Telegram-connected grocery skill that reads your OpenClaw Telegram config and updates local grocery state. Use a dedicated grocery Telegram account, approve only the specific wrapper you need instead of scripts/*.py, and do not approve or run the pruning script unless it is fixed with path containment and a dry-run/confirmation flow.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (2)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
index.js:541
Finding
Telegram Callback Handler Does Not Enforce the Configured User Allowlist<![CDATA[ ## Vulnerability Details **File Location**: `index.js:541-586` **Vulnerability Type**: Missing authorization for state-changing Telegram callbacks **Risk Level**: Medium ### Vulnerable Code ```javascript async function togglePending(state, itemId, target, account, threadId) { const item = state.items[itemId]; if (!item) throw new Error('Grocery item not found.'); item.status = item.status === STATUS_NEEDED ? STATUS_HAVE : STATUS_NEEDED; item.updated_at = utcNow(); await updateAllViews(state, account); return { ok: true, item: { id: item.id, name: item.name, status: item.status } }; } async function handleCallback(state, callback, target, account, threadId) { const parsed = parseCallback(callback); if (!parsed) throw new Error('Unsupported callback payload.'); const [action, value] = parsed; const view = resolveView(state, account, target, threadId); if (action === CALLBACK_TOGGLE) { return togglePending(state, value, target, account, threadId); } if (action === CALLBACK_VIEW) { const mode = value === VIEW_ALL ? VIEW_ALL : VIEW_NEEDED; if (mode === VIEW_NEEDED) view.session_ids = sortedItems(state, STATUS_NEEDED).map(i => i.id); if (view.message_id) return editExistingView(state, target, account, threadId, mode); return sendTelegramView(state, target, account, mode, threadId); } throw new Error('Unsupported callback action.'); } export default function register(api) { api.registerInteractiveHandler({ channel: 'telegram', namespace: 'gchk', handler: async ({ callback, senderId }) => { const target = String(callback.chatId || senderId); const fp = statePath(); let state; try { state = loadState(fp); } catch (err) { console.error('[grocery-checklist] state load error:', String(err)); return; } try { await ha ...[truncated 2815 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Load the `grocery` Telegram account configuration before processing callbacks. 2. Convert every `allowFrom` entry to a canonical string and reject the request unless `String(senderId)` is explicitly present. 3. Fail closed if the account configuration or allowlist cannot be loaded. 4. Verify that the callback's chat identifier corresponds to a stored view belonging to the same account and authorized sender. 5. Avoid treating deterministic item IDs as authorization credentials. 6. Consider binding callback data to a specific account, chat, message, and user through an opaque random value or an authenticated message code. 7. Log rejected callbacks without logging the bot token or other credentials. 8. Add tests proving that unauthorized senders cannot toggle items or switch views. Example authorization control: ```javascript function allowedTelegramUsers(account) { const config = JSON.parse(readFileSync(openclawConfigPath(), 'utf-8')); const allowFrom = config?.channels?.telegram?.accounts?.[account]?.allowFrom || []; return new Set(allowFrom.map(String)); } handler: async ({ callback, senderId }) => { const account = 'grocery'; const sender = String(senderId); if (!allowedTelegramUsers(account).has(sender)) { console.warn('[grocery-checklist] rejected unauthorized callback'); return; } // Continue processing only after authorization succeeds. } ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/prune_grocery_sessions.py:34
Finding
Overbroad Script Approval Includes an Unsafe Session-Pruning Utility<![CDATA[ ## Vulnerability Details **File Locations**: `SETUP.md:61-65`; `scripts/prune_grocery_sessions.py:34-61` **Vulnerability Type**: Excessive execution approval and insufficient filesystem path validation **Risk Level**: Medium ### Vulnerable Code The setup documentation recommends approving every Python file in the Skill directory: ```bash ## 5. Allow exec approvals If your OpenClaw setup requires exec approvals, allowlist: ```bash ~/.openclaw/skills/grocery-checklist/scripts/grocery.sh ~/.openclaw/skills/grocery-checklist/scripts/*.py ``` ``` That wildcard includes `scripts/prune_grocery_sessions.py`, which trusts file paths read from session metadata: ```python def should_prune(meta: dict) -> bool: input_tokens = int(meta.get("inputTokens") or 0) cache_read = int(meta.get("cacheRead") or 0) session_path = Path(str(meta.get("sessionFile") or "")) line_count = session_line_count(session_path) if session_path else 0 return ( input_tokens >= MAX_INPUT_TOKENS or cache_read >= MAX_CACHE_READ or line_count >= MAX_JSONL_LINES ) def archive_session_files(entries: dict) -> None: timestamp = int(time.time()) archive_dir = SESSIONS_DIR / f"archive-auto-{timestamp}" archive_dir.mkdir(parents=True, exist_ok=True) for meta in entries.values(): session_path = Path(str(meta.get("sessionFile") or "")) if session_path.exists(): shutil.move(str(session_path), str(archive_dir / session_path.name)) for backup in SESSIONS_DIR.glob("*.bak.*"): shutil.move(str(backup), str(archive_dir / backup.name)) def main() -> int: sessions = load_sessions() if not sessions: return 0 prune_needed = any(should_prune(meta) for meta in sessions.values()) if not prune_needed: return 0 archive_session_files(sessions) SESSIONS_FILE.write_text("{}\n", encoding="utf-8") return 0 ``` ### Technical Analysis Normal grocery-list operation ...[truncated 2831 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the `scripts/*.py` execution wildcard with the narrowest required approval: ```bash ~/.openclaw/skills/grocery-checklist/scripts/grocery.sh ``` 2. Do not separately approve `grocery.py` if the approved wrapper is sufficient under the OpenClaw execution model. 3. Remove `prune_grocery_sessions.py` from the distributed Skill if it is not part of declared or supported functionality. 4. If session pruning is retained, require explicit administrative invocation rather than general agent execution approval. 5. Resolve every session path and verify containment before reading or moving it: ```python root = SESSIONS_DIR.resolve() candidate = Path(raw_path).expanduser().resolve() try: candidate.relative_to(root) except ValueError: raise RuntimeError("Session file is outside the permitted directory") ``` 6. Require session files to have the expected file type and extension, reject symbolic links where appropriate, and use `lstat` or equivalent checks to prevent symlink-based boundary bypasses. 7. Archive only entries that independently satisfy the pruning threshold instead of moving all sessions when one entry triggers pruning. 8. Update `sessions.json` atomically and preserve entries that were not archived. 9. Add a dry-run mode that lists affected files before any move. 10. Add tests covering absolute paths, `..` traversal, symbolic links, missing paths, duplicate filenames, and metadata referencing files outside the session directory. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
A grocery checklist skill is not expected to prune or archive session files, perform filesystem maintenance on backup/session data, or reset unrelated local state. This mismatch between declared purpose and detected behavior is dangerous because it can conceal destructive or privacy-impacting operations under a benign description, making users and reviewers less likely to notice unauthorized data modification or deletion.

Memory Manipulation

High
Category
Memory Poisoning
Content
if (!state.items || typeof state.items !== 'object') { state.items = {}; return true; }
    let changed = false;
    for (const [itemId, item] of Object.entries(state.items)) {
        if (!item || typeof item !== 'object') { delete state.items[itemId]; changed = true; continue; }
        const name = String(item.name || '').trim();
        const normalized = name ? normalizeName(name) : '';
        if (/^[0-9a-f]{10}$/.test(normalized) && normalized in state.items && normalized !== itemId) {
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Memory Manipulation

High
Category
Memory Poisoning
Content
if (!state.items || typeof state.items !== 'object') { state.items = {}; return true; }
    let changed = false;
    for (const [itemId, item] of Object.entries(state.items)) {
        if (!item || typeof item !== 'object') { delete state.items[itemId]; changed = true; continue; }
        const name = String(item.name || '').trim();
        const normalized = name ? normalizeName(name) : '';
        if (/^[0-9a-f]{10}$/.test(normalized) && normalized in state.items && normalized !== itemId) {
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Memory Manipulation

High
Category
Memory Poisoning
Content
if (!state.items || typeof state.items !== 'object') { state.items = {}; return true; }
    let changed = false;
    for (const [itemId, item] of Object.entries(state.items)) {
        if (!item || typeof item !== 'object') { delete state.items[itemId]; changed = true; continue; }
        const name = String(item.name || '').trim();
        const normalized = name ? normalizeName(name) : '';
        if (/^[0-9a-f]{10}$/.test(normalized) && normalized in state.items && normalized !== itemId) {
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Memory Manipulation

High
Category
Memory Poisoning
Content
if (!state.items || typeof state.items !== 'object') { state.items = {}; return true; }
    let changed = false;
    for (const [itemId, item] of Object.entries(state.items)) {
        if (!item || typeof item !== 'object') { delete state.items[itemId]; changed = true; continue; }
        const name = String(item.name || '').trim();
        const normalized = name ? normalizeName(name) : '';
        if (/^[0-9a-f]{10}$/.test(normalized) && normalized in state.items && normalized !== itemId) {
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Memory Manipulation

High
Category
Memory Poisoning
Content
if (!state.items || typeof state.items !== 'object') { state.items = {}; return true; }
    let changed = false;
    for (const [itemId, item] of Object.entries(state.items)) {
        if (!item || typeof item !== 'object') { delete state.items[itemId]; changed = true; continue; }
        const name = String(item.name || '').trim();
        const normalized = name ? normalizeName(name) : '';
        if (/^[0-9a-f]{10}$/.test(normalized) && normalized in state.items && normalized !== itemId) {
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The README advertises broad natural-language examples like 'I ran out of salt' and 'I bought eggs' without documenting explicit activation boundaries, confirmation requirements, or routing constraints. In a conversational agent context, this can cause unintended state changes from casual chat, quoted text, forwarded messages, or prompt-injected content that resembles pantry commands, especially because the skill is intended for 'normal conversational handling through OpenClaw'.

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill declares broad runtime capabilities via metadata and described behavior, including reading local config, writing persistent state, invoking a shell wrapper, and interacting with Telegram, but it does not explicitly constrain tool scope with permissions or allowed-tools. In an agent environment, this increases the risk of unintended shell, file, or network actions beyond the grocery feature’s stated needs and reduces the platform’s ability to sandbox execution safely.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The mutate_grocery_items tool can remove, rename, merge, and overwrite grocery state and then persists those changes to disk via saveState, but the implementation provides no confirmation prompt or explicit user-facing warning before destructive actions such as remove, rename, or merge. Although the tool description names the actions, there is no inline disclosure at execution time or protective check around irreversible state changes.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = ["openclaw", *args]
    if dry_run:
        return {"ok": True, "dry_run": True, "command": cmd}
    completed = subprocess.run(cmd, capture_output=True, text=True)
    if completed.returncode != 0:
        raise RuntimeError((completed.stderr or completed.stdout or "openclaw command failed").strip())
    stdout = completed.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
84% confidence
Finding
The manifest describes a persistent pantry-backed grocery checklist intended for conversational use with a Telegram shopping-list UI, which justifies rendering checklist content for Telegram interaction. However, this code also reads bot tokens from the user's OpenClaw configuration and performs direct HTTPS requests to Telegram, introducing credential access and outbound network capability that are not stated in the manifest description itself.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
When no target is specified, the skill broadcasts checklist views to every prior active target or every ID in the account allowFrom list. That expands a normal single-user interaction into multi-recipient distribution, which can cause unintended disclosure of grocery/pantry state and unexpected outbound messaging without explicit per-invocation consent.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This code archives session files and then rewrites sessions.json to an empty object, which changes persisted user data. There is no confirmation prompt, logging statement, or explanatory comment/docstring warning the user that session state will be cleared when pruning occurs.

External Transmission

Medium
Category
Data Exfiltration
Content
key: json.dumps(value) if isinstance(value, (dict, list)) else str(value)
        for key, value in payload.items()
    }).encode("utf-8")
    req = request.Request(f"https://api.telegram.org/bot{token}/{method}", data=data, method="POST")
    try:
        with request.urlopen(req, timeout=30) as response:
            raw = response.read().decode("utf-8")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
key: json.dumps(value) if isinstance(value, (dict, list)) else str(value)
        for key, value in payload.items()
    }).encode("utf-8")
    req = request.Request(f"https://api.telegram.org/bot{token}/{method}", data=data, method="POST")
    try:
        with request.urlopen(req, timeout=30) as response:
            raw = response.read().decode("utf-8")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
key: json.dumps(value) if isinstance(value, (dict, list)) else str(value)
        for key, value in payload.items()
    }).encode("utf-8")
    req = request.Request(f"https://api.telegram.org/bot{token}/{method}", data=data, method="POST")
    try:
        with request.urlopen(req, timeout=30) as response:
            raw = response.read().decode("utf-8")
Confidence
60% 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 run_wrapper(args: list[str]) -> dict[str, Any]:
    completed = subprocess.run(
        [str(WRAPPER_PATH), *args],
        capture_output=True,
        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
77% confidence
Finding
The skill loads a bot token from the user's local OpenClaw config and immediately uses it in outbound Telegram API requests. There is no docstring, comment, or user-facing disclosure around accessing credentials or transmitting data to Telegram in this code path.

Static analysis

No suspicious patterns detected.