Back to skill

Security audit

postflight

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent X-posting workflow, but it should be reviewed because it can publish publicly and has concrete local file-handling weaknesses around temporary files and photo ingestion.

Install only if you are comfortable granting the skill controlled access to your X posting workflow and local postflight-state data. Review the temp-file and photo-library path handling before using photo ingestion or concurrent runs, and keep X credentials/session setup under your direct control.

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
PUBLISH-API.md:68
Finding
Predictable Shared Temporary Files Permit Symlink Attacks and Cross-Session Data Corruption<![CDATA[ ## Vulnerability Details **File Location**: `PUBLISH-API.md:68-80, 100-113, 134-153`; `DRAFTING.md:80-83`; `PHOTO-INGESTION.md:66-72` **Vulnerability Type**: Predictable temporary files, symlink following, and unsafe concurrent access **Risk Level**: Medium ### Vulnerable Code `PUBLISH-API.md:68-80`: ```sh xurl media upload "$MEDIA_PATH" > "${TMPDIR:-/tmp}/upload.out" MEDIA_ID="$(jq -Rrs '[split("\n")[] | fromjson? | .data.id? // empty] | last // empty' \ < "${TMPDIR:-/tmp}/upload.out")" ``` Fallback processing uses the same predictable file: ```sh MEDIA_ID="$(grep -oE '"id"[[:space:]]*:[[:space:]]*"[0-9]+"' "${TMPDIR:-/tmp}/upload.out" \ | head -1 | grep -oE '[0-9]+')" ``` `PUBLISH-API.md:100-113`: ```sh cat > "${TMPDIR:-/tmp}/draft.txt" <<'XPOSTER_EOF_3f9c1a' <tweet text verbatim> XPOSTER_EOF_3f9c1a BODY="$(jq -Rsc '{text: rtrimstr("\n")}' < "${TMPDIR:-/tmp}/draft.txt")" xurl -X POST /2/tweets -d "$BODY" ``` The media variant also reads from the same fixed path: ```sh BODY="$(jq -Rsc --arg mid "$MEDIA_ID" \ '{text: rtrimstr("\n"), media: {media_ids: [$mid]}}' < "${TMPDIR:-/tmp}/draft.txt")" xurl -X POST /2/tweets -d "$BODY" ``` `PUBLISH-API.md:134-153`: ```sh cat > "${TMPDIR:-/tmp}/reply.txt" <<'XPOSTER_EOF_3f9c1a' <reply text verbatim> XPOSTER_EOF_3f9c1a RBODY="$(jq -Rsc --arg tid "$TWEET_ID" \ '{text: rtrimstr("\n"), reply: {in_reply_to_tweet_id: $tid}}' < "${TMPDIR:-/tmp}/reply.txt")" xurl -X POST /2/tweets -d "$RBODY" ``` ```text Return the permalink(s) and delete the temp files (`draft.txt`, `reply.txt`, `upload.out`). ``` `DRAFTING.md:80-83`: ```sh cat > "${TMPDIR:-/tmp}/draft.txt" <<'XPOSTER_EOF_3f9c1a' <paste the draft text here, verbatim> XPOSTER_EOF_3f9c1a ``` `PHOTO-INGESTION.md:66-72`: ```sh {baseDir}/ingest-photo.sh --dir "$PWD/postflight-state/<dir>" \ --note-file "${TMPDIR:-/tmp}/note.txt" \ --location-file "${TMPDIR:-/tmp}/loc.txt" \ --name <slug-you-composed> --taken <date-if-override> \ "<staged path> ...[truncated 2853 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a unique private temporary directory for every invocation: ```sh umask 077 tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/postflight.XXXXXXXX")" || exit 1 trap 'rm -rf -- "$tmpdir"' EXIT HUP INT TERM ``` 2. Store every temporary artifact inside that directory: ```sh draft_file="$tmpdir/draft.txt" reply_file="$tmpdir/reply.txt" upload_file="$tmpdir/upload.out" note_file="$tmpdir/note.txt" location_file="$tmpdir/loc.txt" ``` 3. Replace every fixed `${TMPDIR:-/tmp}/<name>` reference with the corresponding unique path. 4. Keep `umask 077` active before creating files containing draft text, locations, notes, API responses, or media identifiers. 5. Ensure cleanup runs on both success and failure through an `EXIT` trap rather than relying on the final workflow step. 6. Do not reuse one temporary directory between concurrent drafting, ingestion, or publishing turns. 7. Where practical, create outputs atomically and reject symbolic links. For script implementations, use safe file descriptors or no-follow/exclusive creation semantics supported by the host platform. 8. Keep the existing quoted heredoc and JSON construction safeguards; they address a different command-injection risk and should remain in place. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
ingest-photo.sh:65
Finding
Photo Ingestion Does Not Enforce Destination Containment<![CDATA[ ## Vulnerability Details **File Location**: `ingest-photo.sh:65, 122-164` **Vulnerability Type**: Insufficient path validation and arbitrary Agent-writable destination access **Risk Level**: Medium ### Vulnerable Code The only destination validation rejects a literal `..` substring: ```sh case "$dir" in *..*) die "--dir must not contain '..'" ;; esac ``` The unchecked destination is subsequently used for directory creation, file copying, metadata rewriting, deletion, and manifest appending: ```sh [[ -n "$dir" ]] || dir="$(default_dir)" mkdir -p "$dir" base="$(basename "$photo")" if [[ -n "$name" ]]; then slug="$name" else slug="$(slugify "${base%.*}")" fi [[ -n "$slug" ]] || slug="photo" dest="$taken-$slug.$ext" if [[ -e "$dir/$dest" ]]; then n=2 while [[ -e "$dir/$taken-$slug-$n.$ext" ]]; do n=$((n + 1)) [[ $n -le 9 ]] || die "too many copies of $taken-$slug — rename the source file" done dest="$taken-$slug-$n.$ext" fi cp "$photo" "$dir/$dest" if ! strip_out="$(exiftool -all= -overwrite_original "$dir/$dest" 2>&1)"; then rm -f "$dir/$dest" die "exiftool could not rewrite $base (${strip_out##*$'\n'}) — not adding it" fi # The verify must fail closed: an exiftool error here is NOT a clean scan. if ! gps="$(exiftool -s3 -gps:all -XMP:Location -IPTC:Sub-location -City "$dir/$dest" 2>&1)"; then rm -f "$dir/$dest" die "could not verify the metadata strip ($gps) — not adding the photo" fi if [[ -n "$gps" ]]; then rm -f "$dir/$dest" die "location metadata survived the strip — not adding the photo" fi ok "EXIF stripped, filed as $dest" tag_list="" for t in "${tags[@]}"; do tag_list="${tag_list:+$tag_list, }\"$t\"" done manifest="$dir/manifest.yaml" if [[ ! -f "$manifest" ]]; then printf '# postflight photo library — one entry per postable photo.\n' > "$manifest" printf '# Maintained by ingest-photo.sh and your editor; the skill only reads it.\n' >> "$manifest" fi { printf -- '- file: %s\n' "$dest" printf ' ta ...[truncated 3506 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Determine and canonicalize the permitted root before processing `--dir`: ```sh workspace="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}" state_root="$workspace/postflight-state" ``` 2. Canonicalize both the state root and requested destination using a platform-appropriate method such as `realpath`, `readlink -f`, or a small Python helper. Account for a destination that does not yet exist by canonicalizing its nearest existing parent. 3. Require the resolved destination to be either the canonical state root or a descendant: ```sh case "$resolved_dir/" in "$resolved_root/"*) ;; *) die "--dir must resolve inside $resolved_root" ;; esac ``` 4. Reject symbolic links in every destination path component. Revalidate after directory creation to reduce time-of-check/time-of-use exposure. 5. Reject a symlinked `manifest.yaml` and symlinked destination filename before writing. Prefer no-follow and exclusive file creation where the platform supports it. 6. Copy to a uniquely named temporary file within the validated directory, strip and verify metadata there, then atomically rename it to the final filename. 7. Avoid overwriting an existing final destination. Use exclusive creation or fail if the selected name appears after collision resolution. 8. Open the manifest safely and append only after verifying that it is a regular file owned or trusted by the expected account. 9. Update the command-line documentation: the requirement must state that `--dir` must canonically resolve beneath `postflight-state`, not merely that it must not contain `..`. 10. Add automated tests for absolute-path escape, symlinked directory escape, symlinked manifest, symlinked destination, non-existent nested paths, and concurrent destination creation. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (13)

Ae1

High
Category
analysis-evasion
Content
`pillars.example.md`, `ingest-photo.sh`. It is **read-only for you.** Never
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
`pillars.example.md`, `ingest-photo.sh`. It is **read-only for you.** Never
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
`pillars.example.md`, `ingest-photo.sh`. It is **read-only for you.** Never
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
`pillars.example.md`, `ingest-photo.sh`. It is **read-only for you.** Never
create, edit, move, or delete anything inside it, for any reason. An
installer replaces this folder wholesale on every upgrade, so anything
written here is deleted without warning. It is not reachable relatively
either — spell it out in full every time (`cat {baseDir}/VOICE.md`).

**`postflight-state/` holds everything else**: settings, the post log,
Confidence
80% confidence
Finding
Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
`pillars.example.md`, `ingest-photo.sh`. It is **read-only for you.** Never
create, edit, move, or delete anything inside it, for any reason. An
installer replaces this folder wholesale on every upgrade, so anything
written here is deleted without warning. It is not reachable relatively
either — spell it out in full every time (`cat {baseDir}/VOICE.md`).

**`postflight-state/` holds everything else**: settings, the post log,
Confidence
80% confidence
Finding
Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
cp "$photo" "$dir/$dest"
if ! strip_out="$(exiftool -all= -overwrite_original "$dir/$dest" 2>&1)"; then
  rm -f "$dir/$dest"
  die "exiftool could not rewrite $base (${strip_out##*$'\n'}) — not adding it"
fi
# The verify must fail closed: an exiftool error here is NOT a clean scan.
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
cp "$photo" "$dir/$dest"
if ! strip_out="$(exiftool -all= -overwrite_original "$dir/$dest" 2>&1)"; then
  rm -f "$dir/$dest"
  die "exiftool could not rewrite $base (${strip_out##*$'\n'}) — not adding it"
fi
# The verify must fail closed: an exiftool error here is NOT a clean scan.
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
cp "$photo" "$dir/$dest"
if ! strip_out="$(exiftool -all= -overwrite_original "$dir/$dest" 2>&1)"; then
  rm -f "$dir/$dest"
  die "exiftool could not rewrite $base (${strip_out##*$'\n'}) — not adding it"
fi
# The verify must fail closed: an exiftool error here is NOT a clean scan.
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Hidden Instructions

High
Category
Prompt Injection
Content
<!-- TEMPLATE — delete this line once you have edited the file. While it is present, the skill ignores this file and runs the default schedule. -->
# Pillar configuration — personal overlay

Copy this file to `postflight-state/pillars.local.md` — not next to this
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Session Persistence

Medium
Category
Rogue Agent
Content
literal string and does NOT support curl's `@file` form — `-d @path`
   sends the characters `@path` as data. Never paste the tweet text inline
   inside `-d '...'` either: quotes in the text would break the command.
   Write the text with a quoted heredoc (the same `draft.txt` from the
   DRAFTING.md length check) and let jq build the body:

   ```sh
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.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The manifest description includes broad triggers such as cron messages that 'mention postflight', general requests for tweet drafts, ship/skip/edit replies, forwarded x.com links, and photos. That ambiguity can cause the skill to activate in unintended contexts, increasing the chance of unauthorized drafting, state changes, or even publishing flows being entered when a different action was intended.

Session Persistence

Medium
Category
Rogue Agent
Content
tail -n 20 postflight-state/post-log.jsonl
```

Write it that way: `postflight-state/...`, no leading path, no `~`, no
directory you worked out yourself. Never `cd` and then use it — if a command
has to run somewhere else, put the `cd` in a subshell
(`(cd postflight-state/media && vhs demo.tape)`) so the next command still
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.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The instructions direct the agent to create temp files for note and location content, run an ingestion script that files the photo into the library, and then delete the temp files. While these operations are central to the skill, the markdown does not explicitly warn the user that their photo and caption-derived data will be written to disk and cleaned up as part of processing.

Static analysis

No suspicious patterns detected.