T02 · Agent Memory Poisoning
- Location
scripts/store.py:58- Finding
Persistent Agent Memory Poisoning Through Untrusted Markdown Content
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This visual-memory skill is mostly coherent, but it needs Review because it persists untrusted visual/page content into agent-readable memory and captures arbitrary URLs without network safety bounds.
Install only if you are comfortable with images and captured webpages being sent to the configured vision provider and stored under ~/.multimodal-memory. Avoid using it on private dashboards, authenticated pages, intranet URLs, localhost, cloud metadata addresses, credentials, or sensitive screenshots. Review or patch URL restrictions, remove the --no-sandbox fallback, pin dependencies, and treat memory.md as untrusted data before enabling automatic session-start loading.
scripts/store.py:58Persistent Agent Memory Poisoning Through Untrusted Markdown Content
scripts/capture_url.py:23Unrestricted URL Capture Permits Server-Side Request Forgery
scripts/capture_url.py:48Chromium Fallback Renders Untrusted Pages With the Browser Sandbox Disabled
requirements.txt:1Unpinned Playwright and Browser Installation Creates Supply-Chain Exposure
The skill instructs the agent to send image content to GPT-4o via API for analysis, but it does not disclose that user images and captured website screenshots will be transmitted to an external service. This is dangerous because screenshots and images can contain sensitive personal, corporate, or credential-bearing information, and users are not given an informed choice before exfiltration occurs.
The documented credential resolution behavior includes harvesting API keys from global OpenClaw config and .env sources, which is broader than necessary for a narrow-purpose skill. Even if only used for API calls here, secret discovery from shared locations is dangerous because it grants the skill access to credentials outside its immediate scope.
1. openclaw.json imageModel -> find matching provider
2. openclaw.json first provider that has an apiKey
3. OPENAI_API_KEY env var -> standard OpenAI endpoint
4. ~/.openclaw/.env OPENAI_API_KEY
"""
cfg = load_openclaw_config()
Reading OPENAI_API_KEY from the environment and then falling back to a user-wide .env file gives the skill access to secrets without explicit scoping to this tool invocation. In an adversarial or compromised skill scenario, this pattern materially increases the chance of secret misuse because the code is permitted to retrieve credentials on its own.
if key:
return key, base_url, model_name
# Fall back to environment / .env file
key = os.environ.get("OPENAI_API_KEY", "")
if not key:
env_file = Path.home() / ".openclaw" / ".env"
The code directly opens and parses ~/.openclaw/.env to extract OPENAI_API_KEY, which is explicit credential access from a shared secrets file. That is dangerous because it normalizes secret scraping by the skill and bypasses stronger separation between the skill's purpose and the user's broader stored credentials.
# Fall back to environment / .env file
key = os.environ.get("OPENAI_API_KEY", "")
if not key:
env_file = Path.home() / ".openclaw" / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
line = line.strip()
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
api_key, base_url, model = resolve_api_config()
if not api_key:
print("ERROR: No API key found. Add OPENAI_API_KEY to ~/.openclaw/.env or configure a provider in openclaw.json.", file=sys.stderr)
sys.exit(1)
print(f"Analyzing {args.image_path} with {model} via {base_url} ...")
The README states that website screenshots are captured and analyzed with GPT-4o or a compatible vision model, which implies that captured webpage content and images may be sent to an external API provider. Without an explicit disclosure, users may unknowingly transmit sensitive webpage content, including authenticated pages or proprietary internal information, to third-party model services.
The README states that website screenshots are captured and analyzed with GPT-4o or a compatible vision model, which implies that captured webpage content and images may be sent to an external API provider. Without an explicit disclosure, users may unknowingly transmit sensitive webpage content, including authenticated pages or proprietary internal information, to third-party model services.
The skill invokes local Python scripts, reads and writes files under the user's home directory, captures websites, and sends data over the network, but it declares no explicit tool scope or permission boundaries. That creates an overbroad execution surface where an agent may use sensitive capabilities without clear policy constraints or user awareness, increasing the risk of unintended file access, persistence, and outbound data transfer.
The skill directs persistent storage of images, screenshots, and metadata in a fixed directory under the user's home folder, but it does not clearly warn the user that this data will be retained across conversations. This can lead to accidental long-term storage of sensitive visual content, URLs, and derived metadata that the user may have expected to be ephemeral.
The instruction tells the agent to ask the user a fallback question in Chinese, regardless of the user's current language or locale. This is a natural-language policy issue because it imposes a specific language choice without offering an alternative or checking user preference.
The skill reads global OpenClaw provider configuration and API credentials from ~/.openclaw/openclaw.json, environment variables, and ~/.openclaw/.env, extending its access beyond the narrow visual-memory function described in the manifest. In this context, that broad credential discovery capability is risky because it allows the skill to leverage unrelated global secrets and arbitrary provider endpoints, increasing the blast radius if the skill is modified or misused.
The code supports transmitting image content to an external model endpoint selected from provider configuration, which means potentially sensitive visual data leaves the local environment. In a memory skill handling screenshots, charts, and website captures, this is security-relevant because such images may contain secrets, personal data, or internal UI details.
if provider_name and provider_name in providers:
p = providers[provider_name]
key = p.get("apiKey", "")
base_url = p.get("baseUrl", "https://api.openai.com/v1")
if key:
return key, base_url, model_name
By iterating over any configured provider with an API key, the skill may send image data to a provider not specifically intended for this action. That expands external transmission risk because the selected endpoint may be less trusted or unrelated to the user's expectations.
# Try any provider that has an apiKey
for p in providers.values():
key = p.get("apiKey", "")
base_url = p.get("baseUrl", "https://api.openai.com/v1")
if key:
return key, base_url, model_name
The default return path hardcodes an external OpenAI endpoint, enabling outbound transfer whenever an API key is found. For a skill that remembers visual content, automatic transmission to a third party is sensitive because images can contain credentials, PII, or confidential business information.
if line.startswith("OPENAI_API_KEY="):
key = line.split("=", 1)[1].strip().strip('"').strip("'")
break
return key, "https://api.openai.com/v1", model_name
def analyze_with_gpt4o(image_path: str, source: str, api_key: str,
The analysis function is explicitly designed to send the full image as base64 to a remote /chat/completions endpoint. This is a true data-exposure concern in context because the skill's purpose is to process user-supplied visual material that may include secrets or proprietary information.
def analyze_with_gpt4o(image_path: str, source: str, api_key: str,
base_url: str = "https://api.openai.com/v1",
model: str = "gpt-4o") -> dict:
import urllib.request
import urllib.error
The natural-language prompt instructs the model to produce the description 'in the same language as any text in the image, default Chinese if ambiguous.' This imposes a specific language choice without user opt-in, which matches the locale/language policy violation category.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
if args.url:
store_cmd += ["--url", args.url]
subprocess.run(store_cmd, check=True)
if result["raw_text"]:
print(f" Detected text: {result['raw_text'][:300]}")
The script accepts a user-controlled URL and launches a real browser to fetch and render it, which creates server-side request forgery style behavior: an attacker can cause the host running the skill to make network requests to arbitrary destinations, including internal services. In the Chrome fallback, the browser is started with --no-sandbox, which weakens process isolation and increases the severity if malicious web content exploits a browser vulnerability during rendering.
if not chrome:
raise FileNotFoundError("Chrome/Chromium not found.")
subprocess.run(
[
chrome,
"--headless",
The dependency is specified with a lower-bound only (playwright>=1.40.0), which allows installation of any newer version, including unreviewed major releases that may introduce breaking changes or supply-chain risk. In a skill that captures and recalls web and visual content, Playwright is a powerful browser automation library, so unexpected dependency drift could materially change runtime behavior or expose the system to newly introduced vulnerabilities.
playwright>=1.40.0