Back to skill

Security audit

Google Analytics 4

Security checks for vulnerabilities and agentic risk

Overview

This GA4 skill is mostly purpose-aligned, but it should be reviewed because its OAuth helper trusts endpoint URLs from the client-secret file while persisting a reusable read-only Analytics token.

Install only if you are comfortable granting read-only access to GA4 reporting data. Create the OAuth Desktop client yourself in Google Cloud, do not use a client_secret.json from another person or website, keep ~/.config/ga4 private, and revoke the Google OAuth grant if the token file or client file may have been exposed. Prefer installing with pinned, reviewed dependency versions in an isolated environment.

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

Error
Location
scripts/ga4_auth.py:65
Finding

OAuth authorization and token endpoints are accepted without provider validation

Content
View full analysis

Vulnerability Details

File Location: scripts/ga4_auth.py, lines 65–76 and 126–149
Vulnerability Type: Unvalidated security-critical OAuth endpoints
Risk Level: High

Technical Analysis

The client credential validator confirms only that auth_uri and token_uri are nonempty strings. It does not verify that they use HTTPS or belong to trusted Google domains. These attacker-controlled values are subsequently passed to InstalledAppFlow, which uses them during authorization and token exchange.

Relevant validation code:

python
def validate_installed_client_file(path_value: str | os.PathLike[str]) -> Path:
    path = require_private_file(path_value)
    try:
        payload: Any = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        raise AuthError("client credential JSON is unreadable or malformed") from exc
    installed = payload.get("installed") if isinstance(payload, dict) else None
    if not isinstance(installed, dict):
        raise AuthError("client credential JSON must contain a Desktop app 'installed' object")
    for field in ("client_id", "client_secret", "auth_uri", "token_uri"):
        if not isinstance(installed.get(field), str) or not installed[field]:
            raise AuthError(f"client credential JSON is missing installed.{field}")
    return path

The unvalidated configuration is then used for OAuth:

python
def authorize(client_file: Path, token_file: Path, port: int) -> Path:
    try:
        from google_auth_oauthlib.flow import InstalledAppFlow
    except ImportError as exc:
        raise AuthError("Google OAuth dependencies are not installed; use requirements.txt") from exc
    try:
        flow = InstalledAppFlow.from_client_secrets_file(str(client_file), scopes=SCOPES)
        credentials = flow.run_local_server(
            host="127.0.0.1",
            port=port,
       
...[truncated 2770 chars]
Remediation
View remediation

Remediation Suggestions

  • Validate security-critical OAuth URLs before constructing InstalledAppFlow.
  • Prefer exact allowlisted provider URLs from genuine Google Desktop client configurations, such as the expected Google authorization and token endpoints.
  • At minimum, require HTTPS, reject user information, fragments, nonstandard ports, redirects to untrusted hosts, and hostnames outside an explicit Google allowlist.
  • Do not rely on suffix checks such as hostname.endswith("google.com"); compare normalized hostnames against exact approved values.
  • Validate the client type and other expected fields, including redirect URI semantics, before beginning authorization.
  • Document that users must download the client file directly from their own Google Cloud Console and must not accept client files supplied by third parties.
  • Where practical, display the normalized authorization and token hosts before opening the browser and require confirmation when they differ from compiled-in defaults.
  • Add automated tests proving that HTTP endpoints, lookalike domains, user-information URL tricks, unexpected ports, and attacker-controlled token endpoints are rejected.
  • If a suspicious client configuration has already been used, revoke the corresponding Google OAuth grant, delete the local token file, obtain a genuine client configuration, and authorize again.

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding

Third-party dependencies are not reproducibly pinned or integrity-verified

Content
View full analysis

Vulnerability Details

File Location: requirements.txt, lines 1–3
Vulnerability Type: Mutable dependency resolution without hashes
Risk Level: Medium

Technical Analysis

All direct dependencies use open-ended lower bounds:

text
google-analytics-data>=0.18.0
google-auth>=2.23
google-auth-oauthlib>=1.2

Each installation can therefore resolve to different current releases and transitive dependency versions. No lock file, upper bounds, package hashes, or controlled package index are provided.

The selected package names are legitimate Google packages, and the audit found no evidence that the current source intentionally references a typosquatted or attacker-operated package. The risk arises because future installation behavior is mutable: a compromised upstream release, compromised maintainer account, malicious transitive update, or incompatible major release could be installed automatically without a source change to this project.

These dependencies execute in the same Python process that reads protected OAuth tokens and sends authenticated GA4 requests. Consequently, malicious package code would execute with the operating-system permissions of the user running the Skill and could access the token after it is loaded.

Attack Path

  1. An upstream direct or transitive dependency publishes a compromised release, or an upstream package-maintainer account or distribution channel is compromised.
  2. A user installs this Skill's requirements after the malicious release becomes the newest version satisfying the lower-bound constraints.
  3. The package installer resolves and installs that release because no exact version or artifact hash prevents it.
  4. The compromised package executes during import, client construction, authentication, or API processing.
  5. It can read the OAuth token available to the process, inspect GA4 report data, send data over the network, or perform other actions allowed to the c ...[truncated 623 chars]
Remediation
View remediation

Remediation Suggestions

  • Generate and commit a reproducible lock or constraints file containing exact versions for direct and transitive dependencies.
  • Record cryptographic hashes for every approved distribution artifact and install with hash enforcement, such as pip install --require-hashes.
  • Use an approved package index or internal mirror rather than allowing arbitrary additional indexes.
  • Review release notes and security advisories before updating locked versions.
  • Perform dependency updates through a controlled process that runs tests and security scanning before merge.
  • Consider separate input and lock files: retain compatible requirements in a source file while generating a fully pinned, hash-verified deployment file.
  • Continue installing dependencies in the isolated environment required by SKILL.md, and run the Skill under a user account with access only to the required credential and output locations.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (12)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 15)May include surrounding context.

md
Install the dependencies declared in `{baseDir}/requirements.txt` in an isolated Python environment. In Google Cloud, enable the Google Analytics Data API and create a **Desktop app** OAuth client using only `analytics.readonly`.

Store the downloaded client JSON at `~/.config/ga4/client_secret.json` or pass a different protected path with `--client-secrets`. The client and token files must be owned by the current user with mode `0600`; each immediate parent directory must reject group/other access, normally mode `0700`. Never paste a client secret, authorization code, refresh token, or access token into chat or a command argument.

Run the supported installed-app loopback flow:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/ga4_auth.py (reported line 18)May include surrounding context.

python
Install the dependencies declared in `{baseDir}/requirements.txt` in an isolated Python environment. In Google Cloud, enable the Google Analytics Data API and create a **Desktop app** OAuth client using only `analytics.readonly`.

Store the downloaded client JSON at `~/.config/ga4/client_secret.json` or pass a different protected path with `--client-secrets`. The client and token files must be owned by the current user with mode `0600`; each immediate parent directory must reject group/other access, normally mode `0700`. Never paste a client secret, authorization code, refresh token, or access token into chat or a command argument.

Run the supported installed-app loopback flow:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 15)May include surrounding context.

md
Install the dependencies declared in `{baseDir}/requirements.txt` in an isolated Python environment. In Google Cloud, enable the Google Analytics Data API and create a **Desktop app** OAuth client using only `analytics.readonly`.

Store the downloaded client JSON at `~/.config/ga4/client_secret.json` or pass a different protected path with `--client-secrets`. The client and token files must be owned by the current user with mode `0600`; each immediate parent directory must reject group/other access, normally mode `0700`. Never paste a client secret, authorization code, refresh token, or access token into chat or a command argument.

Run the supported installed-app loopback flow:

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill declares no explicit tool scope or permission boundary even though its documented operation requires shell execution plus reading and writing local credential and token files. Without an allowlist, an agent runtime may grant broader-than-necessary capabilities, increasing the risk of unintended file access, token exposure, or misuse of shell commands during execution.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
72% confidence
Finding

The skill relies on a locally persisted OAuth token for a Desktop app flow, creating session persistence on disk. Although the scope is read-only and the file permissions guidance is strong, a stolen refresh token could allow continued access to GA4 reporting data until revoked, so the persistence mechanism has real security consequences in shared or weakly secured environments.

Content

Scanner excerpt · SKILL.md (reported line 13)May include surrounding context.

md
## Setup

Install the dependencies declared in `{baseDir}/requirements.txt` in an isolated Python environment. In Google Cloud, enable the Google Analytics Data API and create a **Desktop app** OAuth client using only `analytics.readonly`.

Store the downloaded client JSON at `~/.config/ga4/client_secret.json` or pass a different protected path with `--client-secrets`. The client and token files must be owned by the current user with mode `0600`; each immediate parent directory must reject group/other access, normally mode `0700`. Never paste a client secret, authorization code, refresh token, or access token into chat or a command argument.

Unbounded Resource Access

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Content

Scanner excerpt · SKILL.md (reported line 66)May include surrounding context.

md
## Failure boundary

Malformed metadata, compatibility coverage, headers, rows, row counts, or quota values fail the whole bounded request. Never claim a partial report is complete, broaden the OAuth scope, or retry indefinitely to bypass quota/provider failures.

Official references: [create a report](https://developers.google.com/analytics/devguides/reporting/data/v1/basics), [`runReport`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport), [`getMetadata`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/getMetadata), [`checkCompatibility`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/checkCompatibility), [API schema](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema), and [installed-app OAuth](https://developers.google.com/identity/protocols/oauth2/native-app).

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
94% confidence
Finding

The dependency is specified with only a lower bound, so builds may resolve to different future versions over time. This weakens supply-chain predictability and can introduce breaking changes or newly introduced malicious/compromised releases into the environment without deliberate review.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
google-analytics-data>=0.18.0
google-auth>=2.23
google-auth-oauthlib>=1.2

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
94% confidence
Finding

Using an unpinned authentication library means future installs can silently pull newer versions that have not been validated in this skill. For auth-related packages, that increases supply-chain and reliability risk because behavior or security posture can change unexpectedly between deployments.

Content

Scanner excerpt · requirements.txt (reported line 2)May include surrounding context.

text
google-analytics-data>=0.18.0
google-auth>=2.23
google-auth-oauthlib>=1.2

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
94% confidence
Finding

The OAuth helper library is also unpinned, allowing dependency resolution to drift over time. In a skill that interacts with Google Analytics and likely handles OAuth flows, unreviewed version changes could affect authentication behavior or introduce supply-chain exposure if an upstream release is compromised.

Content

Scanner excerpt · requirements.txt (reported line 3)May include surrounding context.

text
google-analytics-data>=0.18.0
google-auth>=2.23
google-auth-oauthlib>=1.2

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/ga4_query.py (reported line 125)May include surrounding context.

python
def build_filter_expression(specs: list[FilterSpec], types: Any):
    expressions = []
    for spec in specs:
        match_type = getattr(types.Filter.StringFilter.MatchType, spec.match_type)
        leaf = types.FilterExpression(
            filter=types.Filter(
                field_name=spec.field,

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/ga4_query.py (reported line 247)May include surrounding context.

python
except TypeError as exc:
            raise QueryError(f"compatibility response {label}s must be an array") from exc
        for item in iterator:
            metadata = getattr(item, metadata_field, None)
            name = getattr(metadata, "api_name", None)
            if not isinstance(name, str) or not name:
                raise QueryError(f"compatibility response contains a malformed {label}")

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/ga4_query.py (reported line 279)May include surrounding context.

python
raise QueryError("runReport omitted property quota despite returnPropertyQuota")
    result: dict[str, dict[str, int]] = {}
    for name in QUOTA_FIELDS:
        status = getattr(quota, name, None)
        consumed = getattr(status, "consumed", None)
        remaining = getattr(status, "remaining", None)
        if isinstance(consumed, bool) or isinstance(remaining, bool) or not isinstance(consumed, int) or not isinstance(remaining, int):

Static analysis

No suspicious patterns detected.