T09 · Insecure Skill Coding Practices
- Location
scripts/hubspot.mjs:106- Finding
OAuth Access Token Cached Without Restrictive File Permissions
- Content
View full analysis
Vulnerability Details
File Location:
scripts/hubspot.mjs, lines 106–113
Vulnerability Type: Plaintext sensitive credential stored with umask-dependent permissions
Risk Level: MediumVulnerable Code
js // Cache to disk try { const cacheDir = dirname(OAUTH_TOKEN_PATH); if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true }); writeFileSync(OAUTH_TOKEN_PATH, JSON.stringify({ access_token: _cachedAccessToken, expires_at: _tokenExpiresAt, }), 'utf8'); } catch {The destination is defined as:
js const OAUTH_TOKEN_PATH = resolve( homedir(), '.cache', 'openclaw', 'hubspot-token.json' );Technical Analysis
The refreshed OAuth bearer token is stored in plaintext without specifying a restrictive file mode. Consequently, the file's permissions depend on the process umask. On a POSIX system with a permissive or commonly used umask, the resulting file may be readable by other local users or processes.
The cache directory is also created without an explicit
0700mode. The code does not verify the cache file's ownership, permissions, or file type before subsequently reading it. This weakens local credential isolation and may also expose the cache operation to symlink-based file redirection in environments where an attacker can manipulate the cache path.Caching an access token is consistent with the declared OAuth auto-refresh functionality, but storing it without explicit access controls exceeds the minimum local exposure necessary for that functionality.
Attack Path
- A user configures OAuth mode and invokes a HubSpot command.
- The CLI submits the client ID, client secret, and refresh token to HubSpot's official OAuth endpoint.
- HubSpot returns a temporary access token.
- The CLI writes that token to
~/.cache/openclaw/hubspot-token.jsonwithout explicitly setting mode0600. - Another local account or compromised process reads the cache if effective filesystem permissions allo ...[truncated 934 chars]
- Remediation
View remediation
Remediation Suggestions
- Create the private cache directory with owner-only permissions:
js mkdirSync(cacheDir, { recursive: true, mode: 0o700 });- Write the token file with explicit owner-only permissions:
js writeFileSync( OAUTH_TOKEN_PATH, JSON.stringify({ access_token: _cachedAccessToken, expires_at: _tokenExpiresAt, }), { encoding: 'utf8', mode: 0o600 } );-
Explicitly apply
chmodSync(OAUTH_TOKEN_PATH, 0o600)when replacing an existing file, because themodeoption does not necessarily correct permissions on an already existing file. -
Use an atomic write process:
- Create a temporary file in the same private directory using exclusive creation.
- Set mode
0600. - Write and flush the content.
- Rename it atomically to the final cache path.
-
Before reading an existing cache, use
lstatSyncto reject symbolic links and non-regular files. Verify that the file is owned by the current effective user and is not accessible by group or other users. -
Prefer an operating-system credential store or keychain when available. If plaintext caching remains necessary, document its location, permissions, expiration behavior, and secure deletion procedure.
-
Ensure refreshed tokens are revoked or invalidated where supported after suspected local compromise.
