Back to skill

Security audit

Praxis Google Workspace

Security checks for vulnerabilities and agentic risk

Overview

This skill is a coherent Google Workspace CLI, but it asks for sensitive Google account access and has material safety gaps around OAuth warning handling and local token storage.

Review this before installing if you plan to connect a real Google account. Use a least-privilege Google account or test Workspace project, verify the OAuth client yourself before bypassing any Google warning, restrict access to ~/.config/praxis-gws, and revoke the OAuth grant if the token may have been exposed. Prefer a pinned local dependency install over the documented global npm install.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/praxis-gws.js:24
Finding
OAuth credentials and tokens are stored without explicit restrictive filesystem permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/praxis-gws.js:24-29`, `scripts/praxis-gws.js:76-83`, and `scripts/praxis-gws.js:261-269` **Vulnerability Type**: Insecure storage of sensitive OAuth material **Risk Level**: Medium ### Vulnerable Code ```js const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'praxis-gws'); const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json'); const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json'); // Ensure config directory exists fs.mkdirSync(CONFIG_DIR, { recursive: true }); ``` ```js oAuth2Client.getToken(code, (err, token) => { if (err) { console.error('Error retrieving access token', err); process.exit(1); } oAuth2Client.setCredentials(token); fs.writeFileSync(TOKEN_PATH, JSON.stringify(token)); console.log('Token stored to', TOKEN_PATH); resolve(oAuth2Client); }); ``` ```js credentials(srcPath) { if (!fs.existsSync(srcPath)) { console.error('Error: File not found:', srcPath); process.exit(1); } fs.copyFileSync(srcPath, CREDENTIALS_PATH); console.log('Credentials saved to', CREDENTIALS_PATH); console.log('Run any command to start OAuth flow'); }, ``` ### Technical Analysis The configuration directory, OAuth client credentials, and OAuth token are created or copied without explicitly enforcing owner-only permissions. Their effective permissions consequently depend on the process umask and, for copied credentials, filesystem behavior and source-file metadata. `token.json` may contain a long-lived refresh token in addition to a temporary access token. The credentials file contains the OAuth client identifier and client secret. On a multi-user host, container, shared workspace, or environment with a permissive umask, another local account or compromised process may be able to read these files. The implementation also does not validate that the sensitive paths are regular files owned by the current user or reject symbolic links before readi ...[truncated 1573 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the configuration directory with owner-only permissions: ```js fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); fs.chmodSync(CONFIG_DIR, 0o700); ``` 2. Write tokens atomically with mode `0600`. Create a temporary file in the same protected directory, flush it, and rename it into place: ```js const temporaryPath = `${TOKEN_PATH}.tmp-${process.pid}`; fs.writeFileSync(temporaryPath, JSON.stringify(token), { encoding: 'utf8', mode: 0o600, flag: 'wx', }); fs.renameSync(temporaryPath, TOKEN_PATH); fs.chmodSync(TOKEN_PATH, 0o600); ``` 3. After copying OAuth credentials, explicitly restrict their permissions: ```js fs.copyFileSync(srcPath, CREDENTIALS_PATH); fs.chmodSync(CREDENTIALS_PATH, 0o600); ``` 4. Before reading or replacing sensitive files, use `lstatSync` to reject symbolic links and confirm that each path is a regular file owned by the current user. 5. Fail securely if permissions are broader than intended, particularly on shared systems. 6. Document token revocation procedures and advise affected users to revoke existing OAuth grants if token exposure is suspected. ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:157
Finding
Google API dependency is installed globally without version or integrity pinning<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:157-160` **Vulnerability Type**: Unpinned third-party dependency and non-reproducible installation **Risk Level**: Low ### Vulnerable Code ```markdown Requires Node.js and the `googleapis` npm package: ```bash npm install -g googleapis ``` ``` The script subsequently loads the globally installed package from a manually constructed path: ```js const googleapisPath = path.join(process.env.PREFIX || '/usr/local', 'lib/node_modules/googleapis/build/src/index.js'); const { google } = require(googleapisPath); ``` ### Technical Analysis The installation instructions retrieve the mutable latest version of `googleapis` from the configured npm registry. There is no exact version, lockfile, package integrity value, or project-local dependency manifest. Consequently, installation is not reproducible and the code reviewed during the audit may differ from the package installed later. Global installation increases the dependency's exposure and may require elevated installation privileges in some environments. The hard-coded global module path also bypasses conventional project-local dependency resolution and makes execution dependent on externally managed global state. There is no evidence that `googleapis` itself is malicious or typosquatted. The risk arises from trusting an unpinned package release and registry configuration while the loaded dependency receives OAuth credentials and handles highly sensitive Google Workspace data. ### Attack Path 1. A user follows the documented command at a later date. 2. npm resolves the current latest `googleapis` release from the user's configured registry rather than a version reviewed with this Skill. 3. A compromised registry account, malicious registry mirror, dependency-chain compromise, or unsafe future release supplies altered code. 4. The package is installed globally and then loaded by `praxis-gws.js`. 5. The altered package executes in the CLI process and ...[truncated 842 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a project-local `package.json` that pins an exact reviewed `googleapis` version rather than installing the mutable latest version globally. 2. Commit a generated lockfile such as `package-lock.json` so the complete transitive dependency graph and integrity hashes are reproducible. 3. Replace the manually constructed global import path with standard local module resolution: ```js const { google } = require('googleapis'); ``` 4. Install dependencies with a lockfile-enforcing command such as: ```bash npm ci --ignore-scripts ``` Use `--ignore-scripts` where compatible with the selected dependency set and operational requirements. 5. Review dependency provenance, npm registry configuration, integrity metadata, release history, and transitive dependencies before updating the pinned version. 6. Avoid privileged global package installation. Run the CLI and its dependencies as an unprivileged user with access limited to the protected configuration directory. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (16)

Missing User Warnings

High
Confidence
98% confidence
Finding
The skill instructs users to bypass Google's unverified-app warning by clicking through an 'unsafe' prompt, without any compensating security validation steps. This normalizes ignoring platform security warnings and can lead users to authorize a malicious or misconfigured OAuth client with access to email, calendar, and drive data.

Ae1

High
Category
analysis-evasion
Content
scripts/praxis-gws.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'praxis-gws');
const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');

// Ensure config directory exists
fs.mkdirSync(CONFIG_DIR, { recursive: true });
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'praxis-gws');
const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');

// Ensure config directory exists
fs.mkdirSync(CONFIG_DIR, { recursive: true });
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'praxis-gws');
const TOKEN_PATH = path.join(CONFIG_DIR, 'token.json');
const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');

// Ensure config directory exists
fs.mkdirSync(CONFIG_DIR, { recursive: true });
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
function loadCredentials() {
  if (!fs.existsSync(CREDENTIALS_PATH)) {
    console.error('Error: credentials.json not found.');
    console.error('Run: praxis-gws auth credentials /path/to/client_secret.json');
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
function loadCredentials() {
  if (!fs.existsSync(CREDENTIALS_PATH)) {
    console.error('Error: credentials.json not found.');
    console.error('Run: praxis-gws auth credentials /path/to/client_secret.json');
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
function loadCredentials() {
  if (!fs.existsSync(CREDENTIALS_PATH)) {
    console.error('Error: credentials.json not found.');
    console.error('Run: praxis-gws auth credentials /path/to/client_secret.json');
    process.exit(1);
  }
  return JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
rl.close();
      oAuth2Client.getToken(code, (err, token) => {
        if (err) {
          console.error('Error retrieving access token', err);
          process.exit(1);
        }
        oAuth2Client.setCredentials(token);
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
83% confidence
Finding
The skill exposes capabilities that can access environment-derived secrets or runtime configuration, but it does not declare any explicit tool scope or permission boundary. In a skill that handles Gmail, Calendar, and Drive, missing scope documentation increases the risk of unintended data access and makes it harder for users or reviewers to understand what the skill can actually reach.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The documentation describes access to highly sensitive Gmail, Calendar, and Drive data but does not give a clear privacy warning about the breadth of access, token storage, or the consequences of authorizing the app. Users may grant broad OAuth scopes without understanding that email contents, calendar events, and file metadata or contents could be exposed through the CLI.

Session Persistence

Medium
Category
Rogue Agent
Content
## Setup

### 1. Create Google Cloud Project

1. Go to https://console.cloud.google.com
2. Create a new project
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code persists the Google OAuth token to a local file, which is a safety-relevant credential-handling action. While it logs the storage path after writing, there is no prior disclosure in comments, help text, or prompts warning users that authentication tokens will be stored on disk.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The skill defaults calendar event creation to the America/Phoenix timezone when the user does not specify one. This imposes a locale-specific behavior without opt-in, which can violate language/locale policy expectations and lead to incorrect event scheduling for users in other regions.

Unrestricted Tool Access

Medium
Category
Excessive Agency
Content
}
    fs.copyFileSync(srcPath, CREDENTIALS_PATH);
    console.log('Credentials saved to', CREDENTIALS_PATH);
    console.log('Run any command to start OAuth flow');
  },
  
  async status() {
Confidence
80% confidence
Finding
Skill grants unrestricted tool access without appropriate constraints. An agent with unfettered tool access can perform arbitrary actions including file modification, network requests, and code execution.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The command-line handler injects America/Phoenix as the default timezone for calendar creation when --timezone is omitted. This is a locale-specific default applied silently, rather than offering user choice or using account-local settings.

Static analysis

No suspicious patterns detected.