Back to skill

Security audit

outlook-calendar-management

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Outlook calendar purpose, but it has review-worthy risks around automatic unpinned package installation and sensitive OAuth token handling.

Before installing, be comfortable granting full Outlook calendar read/write access. Prefer a dedicated test account or your own Azure app, run the skill in an isolated Python environment with reviewed dependencies already installed, and store token files outside any Git repository. Do not rely on the documented .local-calendar-test path being ignored unless you add and verify an ignore rule yourself.

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 (3)

T08 · Insecure Dependencies

Warning
Location
scripts/ocal_bootstrap.py:15
Finding
Automatic Installation of Unpinned Python Dependencies<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ocal_bootstrap.py:15-15, 41-49` **Vulnerability Type**: Uncontrolled third-party dependency installation **Risk Level**: Medium ### Vulnerable Code ```python # Dependency list: requests (Graph calls) / msal (authentication renewal) / tzdata (Windows timezone data) REQUIRED = ("requests", "msal", "tzdata") def ensure_deps(): """Check dependencies and automatically install missing packages.""" missing = _missing() if not missing: return pkgs = " ".join(missing) print(t("deps_missing", pkgs=pkgs), file=sys.stderr) print(t("deps_installing"), file=sys.stderr) try: proc = subprocess.run( [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", *missing], capture_output=True, text=True, timeout=300, ) ``` ### Technical Analysis Calendar and authentication commands automatically invoke pip when `requests`, `msal`, or `tzdata` is absent. Although the package names are fixed and the subprocess does not use a shell, package versions and integrity hashes are not pinned. Consequently, the effective code installed and executed can change after the Skill has been reviewed. Installation also depends on the active interpreter's pip configuration, including configured package indexes, mirrors, proxy settings, and trusted hosts. A compromised upstream release, package index, mirror, or local pip configuration could supply malicious package content. Python package installation may execute build-system code and installs importable modules that subsequently run in the Agent process. ### Attack Path 1. An attacker compromises a configured package source, mirror, upstream package release, or local pip configuration. 2. At least one required dependency is absent from the runtime environment. 3. A user or Agent invokes a login or calendar command. 4. `ensure_deps()` automatically runs `python -m pip install` without explicit ...[truncated 795 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace unconstrained package names with a reviewed lock file containing exact versions. 2. Require cryptographic hashes by installing with `--require-hashes`. 3. Use a requirements file similar to: ```text requests==<reviewed-version> --hash=sha256:<reviewed-hash> msal==<reviewed-version> --hash=sha256:<reviewed-hash> tzdata==<reviewed-version> --hash=sha256:<reviewed-hash> ``` 4. Restrict installation to an explicitly configured, trusted HTTPS package index. 5. Avoid automatic installation during normal calendar operations. Detect missing dependencies and present an explicit installation command instead. 6. If automatic installation is retained, require affirmative user approval and display the exact package versions and source before invoking pip. 7. Prefer prebuilt, reviewed environments or signed application distributions so runtime package installation is unnecessary. 8. Run dependency vulnerability and provenance checks as part of release and CI processes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ocal_graph.py:47
Finding
Microsoft Graph Bearer Token Can Be Forwarded to an Unvalidated Pagination URL<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ocal_graph.py:47-59, 112-129` **Vulnerability Type**: Authorization-header disclosure through unrestricted absolute URLs **Risk Level**: Medium ### Vulnerable Code ```python headers = {"Authorization": f"Bearer {token}"} # Let Graph return start/end in the local timezone. prefer_parts = [f'outlook.timezone="{LOCAL_TZ_NAME}"'] if prefer_immutable: prefer_parts.append('IdType="ImmutableId"') headers["Prefer"] = ", ".join(prefer_parts) if data: headers["Content-Type"] = "application/json" url = endpoint if endpoint.startswith("http") else f"{GRAPH_BASE}{endpoint}" tz_stripped = False for attempt in range(4): try: resp = requests.request(method, url, headers=headers, json=data, timeout=(10, 30)) ``` ```python def _get_all(url, token, prefer_immutable=False): """Retrieve all paginated results.""" items = [] pages = 0 while url and pages < 200: pages += 1 data = _call("GET", url, token, prefer_immutable=prefer_immutable) items.extend(data.get('value', [])) url = data.get('@odata.nextLink') return items ``` ### Technical Analysis `_call()` treats any string beginning with `http` as an absolute request URL. It does not verify that the URL: - Uses HTTPS. - Has the expected `graph.microsoft.com` hostname. - Uses the expected port. - Remains within the Microsoft Graph API origin. At the same time, `_get_all()` directly trusts the `@odata.nextLink` value returned in a response and passes it back to `_call()`. The same `Authorization: Bearer ...` header is then attached to the resulting request regardless of destination. This creates an origin-confusion weakness. If a malicious or compromised response supplies an off-origin pagination URL, the OAuth bearer token is transmitted to that destination. Checking only `startswith("http")` is insufficient because it accepts arbitrary HTTP and HTTPS origins. Initial application requests are i ...[truncated 1751 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every absolute URL with `urllib.parse.urlsplit`. 2. Require all credential-bearing requests to satisfy: - Scheme exactly `https`. - Hostname exactly `graph.microsoft.com`. - No unexpected username, password, or nonstandard port. 3. Reject off-origin `@odata.nextLink` values before issuing another request. 4. Never attach the `Authorization` header when the destination fails origin validation. 5. Prefer a centralized request helper that accepts only relative Graph paths. If absolute pagination links must be supported, validate and normalize them in a dedicated function. 6. Add tests covering malicious values such as: - `http://graph.microsoft.com/...` - `https://graph.microsoft.com.attacker.example/...` - `https://attacker.example/...` - `https://graph.microsoft.com@attacker.example/...` - Nonstandard ports and mixed-case hostnames. 7. A suitable validation pattern is: ```python from urllib.parse import urlsplit def validate_graph_url(url): parsed = urlsplit(url) if parsed.scheme != "https": raise CalError("Refusing non-HTTPS Graph URL") if parsed.hostname != "graph.microsoft.com": raise CalError("Refusing off-origin Graph URL") if parsed.port not in (None, 443): raise CalError("Refusing unexpected Graph port") return url ``` 8. Consider disabling automatic redirects or validating redirect destinations, because authorization headers must never be forwarded across untrusted origins. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
references/configuration.md:45
Finding
Credential File May Be Committed Because Documentation Incorrectly Claims Git Ignore Protection<![CDATA[ ## Vulnerability Details **File Location**: `references/configuration.md:45-56` **Vulnerability Type**: Unsafe credential-storage guidance **Risk Level**: Low ### Vulnerable Documentation ```markdown ## Separate test login Set `OCAL_TOKEN_PATH` to a separate credential file before running both setup and calendar commands. The parent directory must exist. The same setting must remain present for the integration runner, whose subprocesses inherit it. Without this setting the tool uses `~/.outlook_cal_token.json`. PowerShell example from the project root: ```powershell New-Item -ItemType Directory -Force .local-calendar-test | Out-Null $env:OCAL_TOKEN_PATH = Join-Path (Get-Location) '.local-calendar-test/outlook-token.json' python scripts/outlook_setup.py python scripts/outlook_cal.py status --json ``` This directory is ignored by Git. A separate credential file preserves the usual login, but it does not create a separate calendar: sign in with the intended test account and verify its email before test writes. ``` ### Technical Analysis The documentation directs users to store an OAuth token file under `.local-calendar-test/` inside the project repository and states that this directory is ignored by Git. The audited project structure contains no `.gitignore` file providing that protection. The token file stores authentication results, including access and refresh tokens. Because it resides under the repository, commands such as `git add .` may stage it. Users are more likely to overlook this risk because the documentation explicitly assures them that the directory is ignored. The token file's restrictive local filesystem permissions do not prevent it from being added to a Git repository and uploaded to a remote server. ### Attack Path 1. A developer follows the documented separate-login instructions. 2. `outlook_setup.py` writes access and refresh credentials to: ```text .local-calendar-test/outlook-token.json ``` 3. The developer rel ...[truncated 929 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a project-root `.gitignore` containing: ```gitignore .local-calendar-test/ .outlook_cal_token.json *outlook-token.json ``` 2. Prefer storing credential files outside the repository, such as an operating-system-specific user configuration directory. 3. Amend the documentation to instruct users to verify protection with: ```bash git check-ignore -v .local-calendar-test/outlook-token.json git status --ignored ``` 4. Add a prominent warning that token files contain access and refresh credentials and must never be committed. 5. Add secret-scanning checks to CI and pre-commit workflows. 6. If a token has already been committed, revoke the Microsoft authorization, remove the secret from repository history, and reauthenticate to issue new credentials. 7. Consider using an operating-system credential store instead of plaintext JSON when practical. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (5)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print(t("deps_missing", pkgs=pkgs), file=sys.stderr)
    print(t("deps_installing"), file=sys.stderr)
    try:
        proc = subprocess.run(
            [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", *missing],
            capture_output=True, text=True, timeout=300,
        )
Confidence
90% confidence
Finding
The code automatically invokes pip to install packages at runtime via a subprocess. Although the package names are hardcoded rather than user-controlled, this still expands the skill's capabilities beyond calendar management and creates a supply-chain and unexpected code-execution surface whenever the skill is first run.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill instructs the agent to execute a local Python CLI that can read/write files, access environment variables, invoke shell commands, and make network requests to Microsoft Graph, yet the skill declares no permissions. This creates a transparency and policy-enforcement gap: callers or platform controls may treat the skill as lower risk than it is, while the skill can still perform sensitive calendar writes and authentication flows.

Description-Behavior Mismatch

Medium
Confidence
87% confidence
Finding
The skill requests MailboxSettings.Read in addition to calendar read/write, which exceeds the narrow capability described in the metadata unless timezone lookup is clearly disclosed as required behavior. Overbroad OAuth scopes increase blast radius if the token is stolen or the skill is misused, because mailbox settings and related account metadata become accessible beyond pure calendar management.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
This bootstrapper installs Python packages automatically even though the skill's stated function is Outlook calendar management, not environment modification. Runtime package installation can pull unreviewed code from package indexes, alter the host environment, and surprise operators in ways that are disproportionate to the skill's expected behavior.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The documentation explains that the tool has full calendar read/write access and that changes sync in real time, but it does not prominently warn users that operations can modify, move, or permanently delete events in their real Outlook calendar. In a skill that targets a live personal/work calendar via Microsoft Graph, insufficient warning increases the risk of accidental destructive actions or users authorizing broad access without understanding the consequences.

Static analysis

No suspicious patterns detected.