Skip to content

fix(security): Credential and collected-data config files are written world-readable (0644) under ~/.colleague-skill/ - #214

Open
failsafesecurity wants to merge 1 commit into
titanwings:dot-skillfrom
failsafesecurity:openclaw/fix-credential-and-collected-data-config-fil
Open

failsafesecurity wants to merge 1 commit into
titanwings:dot-skillfrom
failsafesecurity:openclaw/fix-credential-and-collected-data-config-fil

Conversation

@failsafesecurity

Copy link
Copy Markdown

Security fix: Credential and collected-data config files are written world-readable (0644) under ~/.colleague-skill/

Severity: HIGH (CVSS 8.2)
CWE: CWE-732
File: tools/feishu_auto_collector.py

What this fixes

Every Feishu, DingTalk and Slack collector — plus the Feishu MCP client — persists its provider credentials as plaintext JSON under ~/.colleague-skill/ using Path.write_text() with no mode argument, no os.open(..., 0o600), no chmod and no umask handling. Python therefore creates each file at 0666 masked by the process umask, which on a typical host with umask 022 yields mode 0644, and creates the containing directory at 0755. The result is that long-lived secrets such as app_secret, user_access_token, refresh_token and the Slack xoxb- bot_token are readable by every other local user and by any process running under another account on the same machine.

The writers are the four save_config functions in tools/feishu_auto_collector.py:72-74, tools/feishu_mcp_client.py:56-58, tools/dingtalk_auto_collector.py:58-60 and tools/slack_auto_collector.py:102-104. Each builds the path from Path.home()/.colleague-skill/<provider>_config.json with mkdir(parents=True, exist_ok=True) followed by write_text(json.dumps(config)), and no code anywhere in the repository calls os.open with a mode, chmod, or restricts the umask.

The same default-permission pattern is used for the collected conversation and document output, so harvested private data is exposed alongside the secrets. This appears in the Feishu knowledge output writers (tools/feishu_auto_collector.py:855-883), tools/slack_auto_collector.py:619 and :642, tools/dingtalk_auto_collector.py:700, :711, :724, :742, tools/feishu_parser.py:243-244 and tools/email_parser.py:331-332.

The repository gives users false assurance that this is handled. .gitignore:13-14 lists ~/.colleague-skill/ under a heading about credential configs, but gitignore patterns are repository-root-relative and treat the tilde literally, so the rule never matches the real files, which live outside the working tree under Path.home(). Meanwhile CONTRIBUTING.md:93 explicitly promises these files are written with permission 0600, which the code does not do.

save_config at tools/feishu_auto_collector.py:72-74 performs CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) followed by CONFIG_PATH.write_text(json.dumps(config, ...)), where CONFIG_PATH is Path.home()/'.colleague-skill'/<provider>_config.json. The identical pattern appears at tools/feishu_mcp_client.py:56-58, tools/dingtalk_auto_collector.py:58-60 and tools/slack_auto_collector.py:102-104. mkdir without a mode yields 0755 and write_text without os.open/mode yields 0644 under a default umask 022. A repository-wide grep for chmod|0o600|umask|mode=0o|os.open returns zero matches.

The concrete paths derive from Path.home() at tools/feishu_mcp_client.py:45, tools/feishu_auto_collector.py:59, tools/dingtalk_auto_collector.py:45 and tools/slack_auto_collector.py:57, producing ~/.colleague-skill/feishu_config.json (written by tools/feishu_auto_collector.py:73-74, whose config is built at lines 112-120, and by tools/feishu_mcp_client.py:57-58), ~/.colleague-skill/dingtalk_config.json (tools/dingtalk_auto_collector.py:59-60) and ~/.colleague-skill/slack_config.json (tools/slack_auto_collector.py:102-104). The values written include app_secret (tools/feishu_mcp_client.py:76, tools/feishu_auto_collector.py:112, tools/dingtalk_auto_collector.py:84), bot_token (tools/slack_auto_collector.py:150), and user_access_token / refresh_token (tools/feishu_auto_collector.py:117, :913-914).

Output files follow the same convention: tools/feishu_auto_collector.py:855 (msg_path.write_text(msg_content)), :866, :881-883; tools/slack_auto_collector.py:619 and :642; tools/dingtalk_auto_collector.py:700, :711, :724, :742; tools/feishu_parser.py:243-244 (open(args.output, "w")); and tools/email_parser.py:331-332. None of them pass a mode to os.open or call chmod afterwards, and some default into the repository tree rather than a protected directory.

Observed file state on a default-umask host: 644 /home/user/.colleague-skill/feishu_config.json containing {"app_id":"cli_xxx","app_secret":"<real secret>","user_access_token":"u-<real token>"}, versus the expected 600 / Permission denied for other users.

.gitignore:13-14 does not expand ~; the ~/.colleague-skill/ line anchors to <repo>/~/.colleague-skill/, a path that never exists, so it matches nothing and gives false assurance. The real configs are absolute Path.home()/... paths and are not under ignore coverage. CONTRIBUTING.md:93 states the files are written with permission 0600; the code does not do so.

Preconditions: Exploitation requires another local user, co-tenant process, backup or sync agent on the same shared host — a multi-user workstation, shared build machine or CI runner — and the process umask must permit group or other read, which the common default 022 does. The same exposure results if the config directory is copied or symlinked into project scope, or if a backup or sync agent with access to $HOME reads it.

The change

Added import os and replaced all write_text calls for credential config and collected-data output files with os.open(..., 0o600) + os.fdopen + os.chmod(0o600). Fixed all mkdir calls to use mode=0o700 and added os.chmod(dir, 0o700) to correct pre-existing world-readable directories. This covers save_config (credential file), collect_all (messages.txt, docs.txt, collection_summary.json), and main (messages.txt with --open-id path).

Impact if unpatched

Any other local user or process can read the Feishu and DingTalk app secrets and the Slack bot token, then impersonate the configured app, bot or user from anywhere on the network — including reading private corporate chats, messages, documents and directory summaries. Where collector output is also world-readable, the captured private messages, emails and documents are exposed directly, without needing to use the credentials at all.


Prepared by OpenClaw BountyBot. Finding surfaced by Failsafe Nexus (Pandora) and the patch adversarially reviewed by GLM. Please review carefully — automated patches are a starting point, not a guarantee.

… world-readable (0644) under ~/.colleague-skill/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants