T09 · Insecure Skill Coding Practices
- Location
scripts/edit.py:124- Finding
Unvalidated Provider-Controlled URL Retrieval
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This image generation and editing skill mostly matches its stated purpose, but it needs review because it disables a fal.ai safety check and downloads provider-returned URLs without validation.
Review before installing. Use this only with API keys you intend to spend from, avoid sending private images or sensitive prompts unless you trust the selected provider, and be aware that fal.ai editing disables a provider safety filter by default. The URL download handling should be hardened before use in sensitive or network-restricted environments.
scripts/edit.py:124Unvalidated Provider-Controlled URL Retrieval
scripts/edit.py:221fal.ai Image Editing Explicitly Disables the Provider Safety Checker
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
The code follows a URL returned by the OpenAI API and fetches it without validating the scheme, host, or IP range. If the upstream response is compromised or altered, this creates an SSRF-style outbound fetch primitive that could access internal services or unexpected hosts from the runtime environment.
if not b64:
url = result.get("data", [{}])[0].get("url")
if url:
with urllib.request.urlopen(url) as img_resp:
return img_resp.read()
raise RuntimeError("No image data in OpenAI edit response")
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req, timeout=180) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
The code blindly fetches an image from a URL returned by the OpenAI API without validating the scheme, host, or content type. If that upstream response were compromised or unexpected, the skill could be induced to make arbitrary outbound requests, creating an SSRF-style primitive and downloading untrusted content to disk.
# Try URL fallback
url = body.get("data", [{}])[0].get("url")
if url:
with urllib.request.urlopen(url) as img_resp:
return img_resp.read()
raise RuntimeError("No image data in OpenAI response")
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
with urllib.request.urlopen(req, timeout=180) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
The declared purpose emphasizes image generation, but the skill also supports image editing, masking, and processing of local input image files with output written to disk. That mismatch matters because users and policy systems may authorize a generation-only skill while overlooking that it can ingest local files and transform existing content, which broadens the data-handling and privacy exposure.
The declared purpose emphasizes image generation, but the skill also supports image editing, masking, and processing of local input image files with output written to disk. That mismatch matters because users and policy systems may authorize a generation-only skill while overlooking that it can ingest local files and transform existing content, which broadens the data-handling and privacy exposure.
Skill selects an external model or provider that may use a different account or billing plan than the operator expects. Undisclosed model switches can cause unexpected cost or quota consumption.
if not api_key:
raise RuntimeError("GOOGLE_API_KEY not set")
# Default to Flash (cheaper). Use --model gemini-3-pro-image-preview for higher quality.
model = model or "gemini-2.5-flash-image"
aspect = GEMINI_ASPECT_MAP.get(size, "1:1")
The skill documentation advertises behavior that uses environment variables, local file reads/writes, and network access, but it does not declare any explicit tool scope or permissions. This creates a least-privilege and transparency problem: an agent or reviewer cannot easily constrain the skill to only the capabilities it genuinely needs, increasing the chance of unintended credential access, filesystem interaction, or outbound requests.
The trigger description is very broad and can capture many ordinary image-related requests, causing the skill to activate in situations where users may not expect external API calls, file creation, or provider selection based on available keys. Over-broad invocation increases the chance of unnecessary data disclosure to third-party providers and accidental use of local or environmental resources.
The manifest says the skill is for generating images when a user asks to create or make an image, but this file is explicitly an image editor and requires an existing input image to transform. The docstring, provider functions, and CLI all implement image-to-image editing, which is a materially different user-facing behavior from pure image generation.
If an arbitrary model string is supplied, the code uses it directly to construct the fal.run path instead of restricting requests to the declared supported models. This expands the reachable external capability surface and can bypass intended policy controls, cost controls, or safety expectations tied to the documented provider/model scope.
The fal provider explicitly disables the safety checker, which increases the chance of generating disallowed, harmful, or policy-violating content without any user warning. In a skill exposed to end users, disabling provider safeguards materially raises misuse risk and can undermine platform safety guarantees.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
}).encode()
req = urllib.request.Request(
"https://api.openai.com/v1/images/generations",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
}).encode()
req = urllib.request.Request(
"https://api.openai.com/v1/images/generations",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
}).encode()
req = urllib.request.Request(
"https://api.openai.com/v1/images/generations",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
No suspicious patterns detected.