T09 · Insecure Skill Coding Practices
- Location
config.json:2- Finding
Production Credentials Stored in Plaintext Configuration
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill mostly does what it claims, but it ships real-looking credentials and can use them to create TikTok/Postiz drafts through network calls with weak destination safeguards.
Review before installing. Do not use the bundled config.json values; rotate any exposed keys/passwords, replace them with your own least-privilege credentials, restrict file permissions, and only point postiz_base_url at a trusted HTTPS or verified local instance. Treat --auto/cron as unattended social-media publishing workflow and use dry-run/manual review until you are comfortable with the drafts, logs, and external uploads.
config.json:2Production Credentials Stored in Plaintext Configuration
scripts/postiz.py:10Postiz Credentials, Session Cookies, and API Tokens Can Be Sent over Plain HTTP
scripts/generate_slides.py:21Predictable Shared Temporary Paths Permit Symlink-Based File Overwrite
SKILL.md:28Unpinned Runtime Dependencies Are Installed without Integrity Verification
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
payload = {"prompt": prompt, "width": 1024, "height": 1024, "steps": NVIDIA_STEPS}
try:
resp = requests.post(NVIDIA_ENDPOINT, headers=headers, json=payload, timeout=120)
if resp.status_code == 200:
raw_bytes = _b64.b64decode(resp.json()["artifacts"][0]["base64"])
img = Image.open(BytesIO(raw_bytes)).convert("RGB")
The documented autonomous cron-based mode implies ongoing operation using persisted local configuration, API keys, and connected TikTok/Postiz sessions to continue posting across runs. In this context, session persistence is not inherently malicious, but it is security-relevant because compromise of the host, config, or connected Postiz environment could let an attacker post content or abuse affiliated accounts without additional user interaction.
# Manual single post:
"Larry, create a TikTok post for [portal] about [topic]"
# Autonomous mode (via cron):
python3 ~/.openclaw/skills/larry/scripts/larry.py --portal my-portal --auto
The skill documents an autonomous posting mode and persistent performance logging, but it does not present a clear, explicit warning to users that enabling --auto will schedule or upload content to TikTok and store ongoing activity data. This creates a real safety and consent issue: users may trigger external actions and data retention without fully understanding the consequences, increasing the risk of unintended posting, reputational harm, or privacy/compliance problems.
This manifest/config contains multiple natural-language values that target German-language brands and hashtags, such as German domains and German hashtags, but provides no indication that the skill offers a language or locale choice. Under the policy, forcing a specific language or locale without user opt-in can be a natural-language policy violation unless the regional constraint is clearly documented and justified.
This JSON content is entirely authored in German, including the title, slide text, caption hook, and final caption, with no indication that the user can choose another language or that the skill is restricted to German-speaking users. The policy explicitly calls for flagging language or locale constraints that are imposed without user opt-in or clear justification.
This manifest-like JSON contains all user-facing title, slide, caption, and CTA text in German, indicating a fixed language for the skill's generated content. The file does not offer any language or locale choice, nor does it document that the skill is intentionally restricted to German users or a Germany-specific compliance context.
The top-level documentation describes the skill as generating images via NVIDIA FLUX.1-schnell, while the actual endpoint constant at L013 targets black-forest-labs/flux.1-dev. Because the documentation identifies a different model than the one actually called, this is a direct intent-code divergence.
The top-level natural-language description is entirely in German and presents the skill behavior as German by default, with no indication that users may choose another language or that the tool is region-specific. This matches the language/locale policy violation category because it imposes a specific language without opt-in.
The function documentation says it generates images via NVIDIA FLUX.1-schnell, but the configured endpoint at L013 is explicitly for flux.1-dev and _generate_single also documents FLUX.1-dev usage. This is an active contradiction in the code documentation about which model/service is invoked.
The function transmits generated prompts derived from concept, portal, and slide data to an external third-party service. In a skill context, those fields may contain sensitive or proprietary user content, so sending them off-box without explicit consent, minimization, or policy checks can create a real data exposure risk even though the destination appears legitimate.
payload = {"prompt": prompt, "width": 1024, "height": 1024, "steps": NVIDIA_STEPS}
try:
resp = requests.post(NVIDIA_ENDPOINT, headers=headers, json=payload, timeout=120)
if resp.status_code == 200:
raw_bytes = _b64.b64decode(resp.json()["artifacts"][0]["base64"])
img = Image.open(BytesIO(raw_bytes)).convert("RGB")
The script writes draft post data to the queue directory during dry runs and later appends performance data to a persistent log file, but these data-affecting writes are not disclosed in the CLI help or through any confirmation before execution. Although there is a runtime log after the draft write, there is no prior warning that running the skill will create or modify local files.
The non-dry-run path sends post data, including captions and image paths, to an external posting integration via upload_to_tiktok. While posting is part of the skill's purpose, the current interface does not clearly warn at invocation time that live execution will transmit content to an external service unless the user infers it from the script name and comments.
The module title, docstrings, and all user-facing status messages are written in German, indicating the skill is designed to operate in a specific language without offering any user opt-in or alternative locale. The policy for this audit flags language-forcing behavior unless the locale restriction is explicitly justified as region-specific.
The module docstring describes a narrowly scoped integration that uploads TikTok carousels as drafts for later manual publishing. However, the same file also contains check_stats, which reads a local performance log, fetches post analytics from the API, and rewrites the log with updated metrics. That is a different operational intent than the stated upload-only behavior.
This code transmits email and password credentials to a configurable base_url without validating the destination. If an attacker can influence configuration, credentials can be sent to an attacker-controlled endpoint, resulting in account compromise and possible broader service access.
def _get_auth_headers(base_url: str, config: dict) -> dict:
"""Login + Cookie als Header zurückgeben (Postiz Self-Hosted Auth)."""
resp = requests.post(
f"{base_url}/auth/login",
json={"email": config.get("postiz_email", ""),
"password": config.get("postiz_password", ""),
The function sends email and password from configuration to a remote authentication endpoint, which is a safety-relevant network operation involving credentials. While the function has an internal docstring, there is no explicit user-facing disclosure, confirmation, or warning that stored credentials will be used and transmitted over the network.
The function uses a configurable remote API base URL with a production default and then authenticates and uploads media to that endpoint. In an agent-skill context, allowing arbitrary external endpoints materially increases the risk of data exfiltration of credentials and media if configuration is tampered with.
Uploaded Slideshow als TikTok Draft via Postiz.
Self-Hosted: Cookie-basierte Auth statt Bearer Token.
"""
base_url = config.get("postiz_base_url", "https://api.postiz.com/v1")
account_id = portal.get("tiktok_account_id", "")
auth = _get_auth_headers(base_url, config)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
}
try:
resp = requests.post(
f"{base_url}/posts",
headers=headers,
json=payload,
The code reads local image files and uploads them to the Postiz media endpoint, which transfers user data off the local system. Although errors are printed, there is no upfront disclosure in the code warning that local media contents will be sent to an external or self-hosted service.
This function retrieves analytics using an API key from a configurable remote endpoint and writes returned data to local storage. If the endpoint is attacker-controlled, the API key can be disclosed and untrusted response data can be persisted, creating credential exposure and integrity risks.
Ruft Performance-Daten für alle gespeicherten Posts ab.
Wird täglich aufgerufen um Hook-Performance zu tracken.
"""
base_url = config.get("postiz_base_url", "https://api.postiz.com/v1")
api_key = config["postiz_api_key"]
headers = {"Authorization": f"Bearer {api_key}"}
This function reads a bearer API key, retrieves remote analytics for stored posts, and overwrites a local JSON log with updated metrics. The operation combines credential use, network access, and file writes, but the code provides no explicit user-facing disclosure beyond internal comments/docstrings.
The prompt explicitly instructs the model that the generated hook must be 'Auf Deutsch', which hard-codes a language choice. The policy allows locale/language constraints only when the user is offered a choice or the constraint is clearly documented and justified; this file does not show such opt-in or justification.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
Antworte NUR mit dem Hook-Text, nichts weiter."""
try:
result = subprocess.run(
["claude", "-p", prompt],
capture_output=True, text=True, timeout=30
)
With no manifest available, there is no declared scope that would justify launching external processes. The code uses subprocess.run to invoke the local Claude CLI, which is a materially broader capability than pure in-process research/content generation logic and can have side effects outside the module's own data handling.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
}}"""
try:
result = subprocess.run(
["claude", "-p", prompt],
capture_output=True, text=True, timeout=45
)
No suspicious patterns detected.