T07 · Tool Hijacking and Spoofing
- Location
scripts/tweet_poster.py:18- Finding
Posting workflow executes an unaudited script from outside the Skill package
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This skill has a coherent tweet-scheduling purpose, but it uses undocumented credential sources and hard-coded out-of-package execution paths that users should review before installing.
Review carefully before installing. Only use this in an environment where you intentionally allow it to read the listed local credential files, use 1Password/OpenClaw credentials, create scheduled OpenClaw cron jobs, modify the configured Notion database, and post publicly to the configured X account. Prefer fixing the docs/auth mismatch and replacing hard-coded external paths with package-relative paths first.
scripts/tweet_poster.py:18Posting workflow executes an unaudited script from outside the Skill package
scripts/tweet_poster.py:23Skill accesses undocumented password-manager and local OAuth credential stores
The declared behavior says the skill posts via OAuth2, but the implementation reportedly uses OAuth 1.0a for posting and includes a broken or unused OAuth2 refresh helper. This mismatch is dangerous because operators may provision the wrong secrets, misunderstand the trust model, and run unreviewed authentication code paths, which can cause credential misuse, failed auth recovery, or unintended posting behavior.
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.
def notion_headers():
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
key = subprocess.check_output(
["op", "read", "op://OpenClaw/Notion API Key/credential"], env=env
).decode().strip()
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.
def notion_headers():
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
key = subprocess.check_output(
["op", "read", "op://OpenClaw/Notion API Key/credential"], env=env
).decode().strip()
File contents flow to a network sink. This may indicate data exfiltration of sensitive files.
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
for block in blocks:
if block["type"] == "paragraph":
File contents flow to a network sink. This may indicate data exfiltration of sensitive files.
req = urllib.request.Request(
f"https://api.notion.com/v1/pages/{page_id}", headers=headers
)
page = json.loads(urllib.request.urlopen(req).read())
return page["properties"]["Status"]["select"]["name"]
File contents flow to a network sink. This may indicate data exfiltration of sensitive files.
f"https://api.notion.com/v1/pages/{page_id}",
data=data, headers=headers, method="PATCH"
)
urllib.request.urlopen(req)
def load_oauth1_creds():
The function copies the full parent environment and adds a service account token before invoking secret-retrieval subprocesses inside a broken, inconsistent auth-refresh path. Even if not currently called, this broad environment propagation unnecessarily exposes ambient secrets to child processes and increases the blast radius if the refresh path is later enabled or modified incorrectly.
def refresh_twitter_token():
# OAuth1 doesn't need refresh — kept for compat
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
client_id = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/username"], env=env
).decode().strip()
The refresh_twitter_token function performs OAuth2 token exchange and is internally inconsistent and unsafe: it references undefined variables (refresh, user, xurl_path, d), yet would transmit a refresh token and handle new access tokens if ever invoked. Dead or partially migrated auth code like this is dangerous because future callers may enable it and inadvertently break token handling, leak credentials, or corrupt local auth state.
"Authorization": f"Basic {basic}",
"Content-Type": "application/x-www-form-urlencoded",
})
tokens = json.loads(urllib.request.urlopen(req).read())
user["access_token"] = tokens["access_token"]
if "refresh_token" in tokens:
user["refresh_token"] = tokens["refresh_token"]
File contents flow to a network sink. This may indicate data exfiltration of sensitive files.
"Authorization": auth_header,
"Content-Type": "application/json",
})
resp = urllib.request.urlopen(req)
return json.loads(resp.read())
The skill advertises code execution capabilities through metadata (env access, outbound network use, and Python execution) but does not declare an explicit tool scope such as permissions or allowed-tools. That weakens policy enforcement and user visibility, making it easier for the skill to access sensitive environment variables and perform network actions without clear confinement.
The workflow explicitly states that approved overdue tweets are posted immediately, but the skill description does not prominently warn users about this autonomous action. In a content-publishing context, that creates a real risk of accidental or unauthorized publication, especially if a stale approval or timezone mistake causes immediate posting without a final confirmation step.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
from zoneinfo import ZoneInfo
AEST = ZoneInfo("Australia/Sydney")
TWEET_API = "https://api.x.com/2/tweets"
def notion_headers():
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
def notion_headers():
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
key = subprocess.check_output(
["op", "read", "op://OpenClaw/Notion API Key/credential"], env=env
).decode().strip()
return {
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
def notion_headers():
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
key = subprocess.check_output(
["op", "read", "op://OpenClaw/Notion API Key/credential"], env=env
).decode().strip()
return {
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
def notion_headers():
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
key = subprocess.check_output(
["op", "read", "op://OpenClaw/Notion API Key/credential"], env=env
).decode().strip()
return {
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def get_tweet_text(page_id: str, headers: dict) -> str:
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def get_tweet_text(page_id: str, headers: dict) -> str:
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def get_tweet_text(page_id: str, headers: dict) -> str:
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def get_tweet_text(page_id: str, headers: dict) -> str:
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
def get_tweet_text(page_id: str, headers: dict) -> str:
req = urllib.request.Request(
f"https://api.notion.com/v1/blocks/{page_id}/children", headers=headers
)
blocks = json.loads(urllib.request.urlopen(req).read()).get("results", [])
parts = []
The comment says OAuth1 does not need refresh, yet the function implements OAuth2 refresh logic and local file updates with undefined variables. This contradiction strongly suggests abandoned or copy-pasted auth code that can cause insecure future maintenance, token corruption, or accidental secret exposure if someone tries to use it.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
# OAuth1 doesn't need refresh — kept for compat
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
client_id = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/username"], env=env
).decode().strip()
client_secret = subprocess.check_output(
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
# OAuth1 doesn't need refresh — kept for compat
sa = open(os.path.expanduser("~/.config/openclaw/.op-service-token")).read().strip()
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": sa}
client_id = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/username"], env=env
).decode().strip()
client_secret = subprocess.check_output(
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
client_id = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/username"], env=env
).decode().strip()
client_secret = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/credential"], env=env
).decode().strip()
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
client_id = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/username"], env=env
).decode().strip()
client_secret = subprocess.check_output(
["op", "read", "op://OpenClaw/aennkmzygiq2z63vm7rbpmwn6a/credential"], env=env
).decode().strip()
No suspicious patterns detected.