Back to skill

Security audit

Expo App Store Screenshots

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it says, but it documents an unsafe eval-based setup step and includes high-impact store upload actions that can replace public screenshots.

Install only if you trust the projects it will run against and you are comfortable giving the agent simulator/device control plus store-upload credentials. Avoid the documented eval setup on untrusted Expo config, use a virtual environment with pinned dependencies, and require explicit review before any App Store Connect or Google Play upload that may replace screenshots.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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)

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:82
Finding
Arbitrary Shell Command Execution Through Unescaped Configuration Output<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:82`; supporting implementation in `assets/detect-app-config.sh:27-38`, `assets/detect-app-config.sh:41-52`, and `assets/detect-app-config.sh:62-64` **Vulnerability Type**: Command injection through unsafe `eval` **Risk Level**: High ### Vulnerable Code `SKILL.md:82`: ```bash eval "$(bash assets/detect-app-config.sh path/to/app)" ``` `assets/detect-app-config.sh:27-38`: ```bash read_from_jq() { local file="$1" if ! command -v jq >/dev/null 2>&1; then return 1; fi if [[ ! -f "$file" ]]; then return 1; fi scheme=$(jq -r '.expo.scheme // empty' "$file" 2>/dev/null || true) # `scheme` may be an array; pick the first if so if [[ -z "$scheme" ]]; then scheme=$(jq -r '.expo.scheme[0] // empty' "$file" 2>/dev/null || true) fi ios_bundle=$(jq -r '.expo.ios.bundleIdentifier // empty' "$file" 2>/dev/null || true) android_pkg=$(jq -r '.expo.android.package // empty' "$file" 2>/dev/null || true) [[ -n "$scheme" || -n "$ios_bundle" || -n "$android_pkg" ]] } ``` `assets/detect-app-config.sh:41-52`: ```bash read_from_expo_cli() { if ! command -v npx >/dev/null 2>&1; then return 1; fi local json json=$(npx --no-install expo config --type public --json 2>/dev/null) || return 1 if ! command -v jq >/dev/null 2>&1; then return 1; fi scheme=$(printf '%s' "$json" | jq -r '.scheme // empty' 2>/dev/null || true) if [[ -z "$scheme" ]]; then scheme=$(printf '%s' "$json" | jq -r '.scheme[0] // empty' 2>/dev/null || true) fi ios_bundle=$(printf '%s' "$json" | jq -r '.ios.bundleIdentifier // empty' 2>/dev/null || true) android_pkg=$(printf '%s' "$json" | jq -r '.android.package // empty' 2>/dev/null || true) [[ -n "$scheme" || -n "$ios_bundle" || -n "$android_pkg" ]] } ``` `assets/detect-app-config.sh:62-64`: ```bash printf 'APP_SCHEME=%s\n' "$scheme" printf 'IOS_BUNDLE_ID=%s\n' "$ios_bundle" printf 'ANDROID_PACKAGE=%s\n' "$android_pkg" ``` ### Technical Analysis The dete ...[truncated 2334 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the documented use of `eval`. 2. Have the detection script return a structured format such as JSON: ```bash jq -n \ --arg scheme "$scheme" \ --arg ios "$ios_bundle" \ --arg android "$android_pkg" \ '{APP_SCHEME: $scheme, IOS_BUNDLE_ID: $ios, ANDROID_PACKAGE: $android}' ``` 3. Parse each value as data rather than executable shell text: ```bash config_json="$(bash assets/detect-app-config.sh path/to/app)" APP_SCHEME="$(jq -r '.APP_SCHEME' <<<"$config_json")" IOS_BUNDLE_ID="$(jq -r '.IOS_BUNDLE_ID' <<<"$config_json")" ANDROID_PACKAGE="$(jq -r '.ANDROID_PACKAGE' <<<"$config_json")" ``` 4. Validate values using conservative allowlists before using them. Reject control characters, whitespace, shell metacharacters, and values outside the expected scheme or identifier syntax. 5. If shell assignments must be retained for compatibility, serialize every value with `printf '%q'`; however, direct structured parsing without `eval` is preferred. 6. Add regression tests using values containing spaces, quotes, semicolons, backticks, and command substitutions, verifying that no command is executed. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:184
Finding
Unpinned Third-Party Dependencies Create Supply-Chain and Reproducibility Risk<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:184` and `SKILL.md:214`; duplicated in `assets/upload-app-store.py:41` and `assets/upload-play-store.py:38` **Vulnerability Type**: Unpinned runtime dependency installation **Risk Level**: Medium ### Vulnerable Code `SKILL.md:184`: ```bash pip install 'pyjwt[crypto]' requests ``` `SKILL.md:214`: ```bash pip install google-auth requests ``` The same installation guidance appears in the upload helper documentation: `assets/upload-app-store.py:40-41`: ```text Requires: requests, pyjwt[crypto]. pip install 'pyjwt[crypto]' requests ``` `assets/upload-play-store.py:37-38`: ```text Requires: requests, google-auth. pip install google-auth requests ``` ### Technical Analysis The installation commands resolve mutable latest versions of direct and transitive dependencies at execution time. No reviewed versions, lock file, hashes, or isolated environment are specified. This does not demonstrate that the named packages are malicious. However, it means that the code ultimately imported by the upload scripts can change after the skill has been audited. A compromised upstream release, compromised package-index account, malicious transitive dependency, or incompatible update could execute code during installation or import. The upload helpers handle high-value credentials and authenticated store operations. Therefore, dependency integrity is particularly important even though the package names shown are legitimate and the network destinations in the audited scripts are official Apple and Google APIs. ### Attack Path 1. A user follows the documented `pip install` command. 2. The package index resolves the newest available versions and their current dependency trees. 3. A compromised, replaced, or unexpectedly modified package release is downloaded because no version or artifact hash is enforced. 4. Package installation hooks or imported package code executes on the workstation. 5. Malicious depend ...[truncated 834 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create separate, reviewed dependency files for the upload helpers with exact version pins. 2. Generate and record cryptographic hashes for every direct and transitive package. 3. Install with hash verification, for example: ```bash python3 -m pip install --require-hashes -r requirements-upload.txt ``` 4. Use a project-specific virtual environment rather than a global Python installation: ```bash python3 -m venv .venv . .venv/bin/activate python3 -m pip install --require-hashes -r requirements-upload.txt ``` 5. Review dependency updates before regenerating the lock file and hashes. 6. Consider separating App Store and Google Play dependencies so each helper installs only the packages it requires. 7. Document the supported Python version and test the locked environment in continuous integration. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (25)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The code chunk does not implement the declared screenshot-generation behavior. Instead, it performs route discovery for Expo Router projects by walking the filesystem and emitting route metadata. While route enumeration could be a supporting step in a broader screenshot workflow, this chunk’s primary and only observable behavior is route detection, not screenshot capture/preparation. Therefore the description materially overstates and misrepresents what this code actually does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description promises a comprehensive screenshot generation pipeline for app stores, including device/simulator control and screenshot capture. The actual code only implements one narrow supporting step: resizing already-existing PNG images with ImageMagick. Resizing is consistent with a small subset of the description, but the primary purpose and most advertised capabilities are absent from this code chunk. Therefore the description does not accurately represent what this specific code actually does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The description presents a broad screenshot-generation automation skill centered on capturing and preparing store screenshots across iOS and Android. This code chunk does not perform capture or preparation. Instead, its primary behavior is App Store Connect management: API authentication, locating an editable iOS version, ensuring localization/screenshot-set existence, deleting existing screenshots unless told not to, and uploading local PNGs. While the description briefly mentions uploading screenshots to App Store Connect or Google Play as a use case, this code is much narrower than the declared primary purpose and lacks the core simulator/device-driving and image-preparation behavior. Therefore the description does not accurately represent what this code chunk actually does.

Ae1

High
Category
analysis-evasion
Content
| [`assets/write-summary.sh`](assets/write-summary.sh) | Write `summary.md` into a device folder (model, OS, resolution, screen list). |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| [`assets/write-summary.sh`](assets/write-summary.sh) | Write `summary.md` into a device folder (model, OS, resolution, screen list). |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| [`assets/write-summary.sh`](assets/write-summary.sh) | Write `summary.md` into a device folder (model, OS, resolution, screen list). |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| [`assets/write-summary.sh`](assets/write-summary.sh) | Write `summary.md` into a device folder (model, OS, resolution, screen list). |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill documents capabilities that read environment variables, access local files, and perform network uploads, but it does not declare any explicit tool scope or permission boundary. In an agent setting, that makes the skill easier to invoke with broader-than-expected authority, increasing the risk of unintended credential access or remote modification of App Store / Play listing data.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The trigger description includes broad adjacent phrasing that could cause the skill to activate for loosely related screenshot requests. In context, this skill can lead to device control, file writes, environment-secret use, and optional store uploads, so over-broad activation increases the chance of unintended high-impact actions.

Rp1

Medium
Category
MCP Rug Pull
Confidence
81% confidence
Finding
Referencing `npx expo` without a pinned version can cause the skill to execute whatever package version is currently resolved from the environment or registry. That creates a supply-chain risk where behavior changes unexpectedly or a malicious package version could be pulled and run during detection.

Rp1

Medium
Category
MCP Rug Pull
Confidence
81% confidence
Finding
The detection helper again relies on unpinned `npx expo`, which inherits the same supply-chain and reproducibility risks. In a skill that may be run automatically, executing an unpinned remote package is more dangerous because it can happen without close user review.

Whitespace Padding

Medium
Category
Prompt Injection
Content
## Store target dimensions

| Device          | Required size | Notes                                                                                    |
| --------------- | ------------- | ---------------------------------------------------------------------------------------- |
| `iphone`        | 1284×2778     | App Store 6.5" display. Capture on iPhone 16 Pro Max sim (1320×2868) and resize.         |
| `ipad`          | 2064×2752     | App Store 13" display. iPad Pro 13" M4 captures natively at this size.                   |
Confidence
70% confidence
Finding
Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Whitespace Padding

Medium
Category
Prompt Injection
Content
## Scripts (all live under `assets/`)

| Script                                                                                       | Purpose                                                                                                  |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [`assets/detect-app-config.sh`](assets/detect-app-config.sh)                 | Discover `APP_SCHEME`, `IOS_BUNDLE_ID`, `ANDROID_PACKAGE` from the Expo config.                          |
| [`assets/detect-routes.sh`](assets/detect-routes.sh)                         | Walk an Expo Router `app/` (or `src/app/`) tree and print every route as `<url>\t<group>\t<source-file>`. |
Confidence
70% confidence
Finding
Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Whitespace Padding

Medium
Category
Prompt Injection
Content
## Scripts (all live under `assets/`)

| Script                                                                                       | Purpose                                                                                                  |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [`assets/detect-app-config.sh`](assets/detect-app-config.sh)                 | Discover `APP_SCHEME`, `IOS_BUNDLE_ID`, `ANDROID_PACKAGE` from the Expo config.                          |
| [`assets/detect-routes.sh`](assets/detect-routes.sh)                         | Walk an Expo Router `app/` (or `src/app/`) tree and print every route as `<url>\t<group>\t<source-file>`. |
Confidence
70% confidence
Finding
Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The App Store upload workflow instructs use of sensitive API credentials and performs remote modification of store assets, but it does not prominently warn about secret handling, least privilege, or the destructive replace-by-default behavior. In an agent context, that raises the risk of credential leakage through logs/env inspection and unintended production metadata changes.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The Google Play workflow similarly uses a highly sensitive service-account key to modify public store listing screenshots without a prominent warning about credential sensitivity and side effects. Because the script opens and commits store edits, misuse can alter production-facing metadata and expose privileged cloud credentials if handled unsafely.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
95% confidence
Finding
The script executes `npx --no-install expo config --type public --json`, which relies on whatever `expo` CLI is available in the local project or environment without validating a pinned version. In a security-sensitive automation context, this can execute an unexpected or compromised CLI version, and `expo config` may evaluate dynamic app config code such as `app.config.js/ts`, increasing the risk of arbitrary code execution from an untrusted repository.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill manifest focuses on capturing and preparing screenshots on simulators/emulators and resizing them, with upload mentioned only as a secondary trigger phrase. This script performs substantial remote management of App Store Connect state: it creates appStoreVersionLocalizations and appScreenshotSets, deletes existing screenshots, and uploads new assets via the App Store Connect API.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
For a skill presented primarily as driving simulators/devices to capture marketing screenshots, handling external API credentials and performing authenticated remote API mutations is a distinct capability. While upload support is mentioned in the manifest, credential-driven integration with App Store Connect is not clearly scoped as part of the skill's main behavior.

External Transmission

Medium
Category
Data Exfiltration
Content
import jwt
import requests

BASE = "https://api.appstoreconnect.apple.com/v1"

# device shorthand -> ASC screenshotDisplayType
DEVICE_DISPLAY_TYPE = {
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'url' from requests.post (line 115, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
for p in pngs:
        data = p.read_bytes()
        url = f"{upload_edits_url}{listing_path}?uploadType=media"
        r = requests.post(
            url,
            headers={**h, "Content-Type": "image/png"},
            data=data,
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: 'edit_id' from requests.post (line 101, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
sys.exit(f"upload {p.name} → {r.status_code}\n{r.text}")
        print(f"  uploaded {p.name}")

    r = requests.post(f"{edits_url}/{edit_id}:commit", headers=h)
    if not r.ok:
        sys.exit(f"commit edit → {r.status_code}\n{r.text}")
    print(f"done — committed {len(pngs)} images to {args.locale}/{args.image_type}")
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.

Missing User Warnings

Low
Confidence
85% confidence
Finding
This code creates local directories/files and deletes a temporary file on the connected Android device, but it does not emit any confirmation, logging, or explicit warning at execution time about those side effects. While these actions are part of the script's purpose, the device-side deletion and local write behavior are only implicit in the implementation and not disclosed to the user except through minimal comments.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
args = ap.parse_args()

    for attr, flag in (("key_id", "--key-id"), ("issuer_id", "--issuer-id"), ("key_path", "--key-path")):
        if not getattr(args, attr):
            sys.exit(f"missing {flag} (or ASC_{attr.upper()} env var)")

    pngs = sorted(Path(args.dir).glob("*.png"))
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

No suspicious patterns detected.