Skip to content

Batch remaining hosted egress hardening fixes#1106

Merged
RhysSullivan merged 5 commits into
mainfrom
fix/microsoft-graph-url-allowlist
Jun 23, 2026
Merged

Batch remaining hosted egress hardening fixes#1106
RhysSullivan merged 5 commits into
mainfrom
fix/microsoft-graph-url-allowlist

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What changed

Included fixes

  • Route OAuth token HTTP through the hosted egress guard.
  • Route MCP transports through the hosted HttpClient.
  • Use the executor HTTP layer for GraphQL discovery.
  • Restrict Google Discovery bundle URLs.
  • Pin Microsoft Graph provider URLs.

Validation

  • Earlier CI passed on each individual PR before the final batch retarget.
  • Batch merge requested without waiting for the retargeted CI cycle.

Stack

Base: main

Skipped: #1104

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 0cf12bc Commit Preview URL

Branch Preview URL
Jun 23 2026, 06:15 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 0cf12bc Jun 23 2026, 06:17 PM

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1106

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1106

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1106

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1106

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1106

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1106

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1106

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1106

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1106

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1106

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1106

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1106

executor

npm i https://pkg.pr.new/executor@1106

commit: b6f0590

@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from 8a56f37 to be395f6 Compare June 23, 2026 17:00
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from e023f2c to ac0bda3 Compare June 23, 2026 17:00
@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from be395f6 to 94c590f Compare June 23, 2026 17:02
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from ac0bda3 to 9869c7b Compare June 23, 2026 17:02
@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from 94c590f to 7a69015 Compare June 23, 2026 17:08
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from 9869c7b to caf9cb7 Compare June 23, 2026 17:08
@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from 7a69015 to e9aff2c Compare June 23, 2026 17:36
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from caf9cb7 to 64419c5 Compare June 23, 2026 17:36
@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from e9aff2c to 54782a5 Compare June 23, 2026 18:01
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from 64419c5 to b6f0590 Compare June 23, 2026 18:01
@RhysSullivan RhysSullivan marked this pull request as ready for review June 23, 2026 18:02
@RhysSullivan RhysSullivan force-pushed the fix/google-discovery-url-allowlist branch from 54782a5 to f275c2c Compare June 23, 2026 18:11
@RhysSullivan RhysSullivan force-pushed the fix/microsoft-graph-url-allowlist branch from b6f0590 to 0cf12bc Compare June 23, 2026 18:11
@RhysSullivan RhysSullivan changed the title Pin Microsoft Graph provider URLs Batch remaining hosted egress hardening fixes Jun 23, 2026
@RhysSullivan RhysSullivan changed the base branch from fix/google-discovery-url-allowlist to main June 23, 2026 18:11
@RhysSullivan RhysSullivan merged commit c160b48 into main Jun 23, 2026
13 of 20 checks passed
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens the Microsoft Graph plugin against SSRF-style bearer-token exfiltration by validating all user-supplied URLs — spec URL, base URL, and all three OAuth endpoint URLs — against explicit allowlists before any outbound HTTP request or persistence operation. An opt-in allowUnsafeUrlOverrides flag retains flexibility for local emulators and test environments.

  • graph.ts adds parseTrustedHttpsUrl, normalizeMicrosoftGraphSpecUrl, normalizeMicrosoftGraphBaseUrl, and normalizeMicrosoftOAuthEndpointUrl helpers backed by MICROSOFT_GRAPH_HOSTS and MICROSOFT_IDENTITY_HOSTS sets, plus two Effect-based validation steps (validateSelectionUrls before the first fetch, validateResolvedOAuthEndpoints after spec-sourced endpoints are resolved).
  • plugin.ts threads MicrosoftGraphUrlPolicy through makeMicrosoftPluginExtension, addGraph, and updateGraph; always coerces allowUnsafeUrlOverrides to === true so the boolean cannot be forged by a downstream caller.
  • plugin.test.ts adds a zero-fetch regression that proves the rejection occurs before the HTTP client is touched, and gates the existing emulator test behind allowUnsafeUrlOverrides: true.

Confidence Score: 4/5

The change is safe to merge — all URL validation runs before any outbound fetch and before persistence, and the existing test suite confirms the zero-fetch rejection path works end-to-end.

The validation logic is structurally sound: spec URL, base URL, and OAuth endpoints are each checked against explicit allowlists before any HTTP call, and the allowUnsafeUrlOverrides bypass is gated behind a strict boolean equality check at the plugin boundary. The remaining gaps are cosmetic or minor: raw config.baseUrl is persisted instead of the normalized graph.baseUrl, non-standard ports on trusted hostnames pass parseTrustedHttpsUrl, and the updateGraph override-rejection path lacks a dedicated test.

plugin.ts lines 174 and 241 where the raw config.baseUrl/input.baseUrl is written to the integration config instead of the normalized graph.baseUrl.

Important Files Changed

Filename Overview
packages/plugins/microsoft/src/sdk/graph.ts Adds URL allowlist validation for spec, base, and OAuth endpoint URLs before any outbound fetch; overall logic is sound but parseTrustedHttpsUrl accepts non-standard ports on trusted hostnames.
packages/plugins/microsoft/src/sdk/plugin.ts Threads urlPolicy through the plugin correctly; addGraph and updateGraph both store the raw config.baseUrl/input.baseUrl instead of the normalized graph.baseUrl, making the persisted config inconsistent with the validated form.
packages/plugins/microsoft/src/sdk/plugin.test.ts Adds a zero-fetch rejection test for addGraph and correctly gates the emulator test behind allowUnsafeUrlOverrides: true; the updateGraph rejection path remains untested.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["addGraph / updateGraph input"] --> B["normalizeSelection()"]
    B --> C["validateSelectionUrls()"]
    C --> D{"specUrl == MICROSOFT_GRAPH_OPENAPI_URL?"}
    D -- "yes" --> E["specUrl accepted"]
    D -- "no" --> F{"allowUnsafeUrlOverrides?"}
    F -- "true + HTTPS" --> E
    F -- "false / non-HTTPS" --> ERR1["❌ OpenApiParseError"]
    E --> G{"baseUrl provided?"}
    G -- "yes" --> H{"allowUnsafeUrlOverrides?"}
    H -- "true + HTTPS" --> I["baseUrl accepted (any HTTPS)"]
    H -- "false" --> J{"host in MICROSOFT_GRAPH_HOSTS + /v1.0 or /beta path?"}
    J -- "yes" --> I
    J -- "no" --> ERR2["❌ OpenApiParseError"]
    G -- "no" --> K["baseUrl = undefined"]
    I --> L["validateSelectionUrls checks authorizationUrl / tokenUrl / clientCredentialsTokenUrl against MICROSOFT_IDENTITY_HOSTS"]
    K --> L
    L --> M["fetchMicrosoftGraphOpenApiSpec(specUrl)"]
    M --> N["resolveOAuthEndpoints(headDoc, selection)"]
    N --> O["validateResolvedOAuthEndpoints()"]
    O --> P{"allowUnsafeUrlOverrides?"}
    P -- "true + HTTPS" --> Q["OAuth endpoints accepted"]
    P -- "false" --> R{"host in MICROSOFT_IDENTITY_HOSTS + /tenant/oauth2/v2.0/* path?"}
    R -- "yes" --> Q
    R -- "no" --> ERR3["❌ OpenApiParseError"]
    Q --> S["MicrosoftGraphSpecBuild returned"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["addGraph / updateGraph input"] --> B["normalizeSelection()"]
    B --> C["validateSelectionUrls()"]
    C --> D{"specUrl == MICROSOFT_GRAPH_OPENAPI_URL?"}
    D -- "yes" --> E["specUrl accepted"]
    D -- "no" --> F{"allowUnsafeUrlOverrides?"}
    F -- "true + HTTPS" --> E
    F -- "false / non-HTTPS" --> ERR1["❌ OpenApiParseError"]
    E --> G{"baseUrl provided?"}
    G -- "yes" --> H{"allowUnsafeUrlOverrides?"}
    H -- "true + HTTPS" --> I["baseUrl accepted (any HTTPS)"]
    H -- "false" --> J{"host in MICROSOFT_GRAPH_HOSTS + /v1.0 or /beta path?"}
    J -- "yes" --> I
    J -- "no" --> ERR2["❌ OpenApiParseError"]
    G -- "no" --> K["baseUrl = undefined"]
    I --> L["validateSelectionUrls checks authorizationUrl / tokenUrl / clientCredentialsTokenUrl against MICROSOFT_IDENTITY_HOSTS"]
    K --> L
    L --> M["fetchMicrosoftGraphOpenApiSpec(specUrl)"]
    M --> N["resolveOAuthEndpoints(headDoc, selection)"]
    N --> O["validateResolvedOAuthEndpoints()"]
    O --> P{"allowUnsafeUrlOverrides?"}
    P -- "true + HTTPS" --> Q["OAuth endpoints accepted"]
    P -- "false" --> R{"host in MICROSOFT_IDENTITY_HOSTS + /tenant/oauth2/v2.0/* path?"}
    R -- "yes" --> Q
    R -- "no" --> ERR3["❌ OpenApiParseError"]
    Q --> S["MicrosoftGraphSpecBuild returned"]
Loading

Comments Outside Diff (2)

  1. packages/plugins/microsoft/src/sdk/plugin.ts, line 174 (link)

    P2 Raw config.baseUrl stored instead of the normalized graph.baseUrl

    graph.specUrl and all three OAuth URLs are taken from the graph result (normalized and validated by validateSelectionUrls), but baseUrl is written back from config.baseUrl — the raw user-supplied string. normalizeMicrosoftGraphBaseUrl strips trailing slashes and lower-cases the hostname, so a caller that passes "https://graph.microsoft.COM/v1.0/" would have the inconsistent raw value persisted in config while every other field is normalized. Using graph.baseUrl keeps the persisted config consistent with what was validated.

  2. packages/plugins/microsoft/src/sdk/plugin.ts, line 241 (link)

    P2 Same raw-input persistence concern in updateGraphinput.baseUrl should be replaced by graph.baseUrl so the stored config always reflects the normalized, post-validation form.

Reviews (1): Last reviewed commit: "Pin Microsoft Graph provider URLs" | Re-trigger Greptile

Comment on lines +187 to +194
const parseTrustedHttpsUrl = (value: string): URL | null => {
if (!URL.canParse(value)) return null;
const parsed = new URL(value);
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) {
return null;
}
return parsed;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-standard ports accepted on trusted hostnames

parseTrustedHttpsUrl extracts parsed.hostname, which strips the port component before the allowlist lookup runs. This means a URL like https://graph.microsoft.com:8443/v1.0 would pass all three layers of validation (HTTPS check, host allowlist, pathname regex) as a baseUrl, and https://login.microsoftonline.com:1234/tenant/oauth2/v2.0/token would pass as an OAuth endpoint. In the production path, an attacker doesn't control those hostnames so there's no exfiltration path, but an overly-permissive URL is still silently persisted. Checking parsed.port === "" || parsed.port === "443" (or equivalently, that parsed.host === parsed.hostname) before returning would close the gap without breaking any real use-case.

Comment on lines +201 to +240
it.effect("rejects non-Microsoft URL overrides before fetching the Graph spec", () =>
Effect.scoped(
Effect.gen(function* () {
let requests = 0;
const blockedHttpClientLayer = Layer.succeed(HttpClient.HttpClient)(
HttpClient.make((request: HttpClientRequest.HttpClientRequest) =>
Effect.sync(() => {
requests += 1;
return HttpClientResponse.fromWeb(
request,
new Response("unexpected request", { status: 500 }),
);
}),
),
);
const executor = yield* createExecutor(
makeTestConfig({
plugins: [
microsoftPlugin({ httpClientLayer: blockedHttpClientLayer }),
memoryCredentialsPlugin(),
],
}),
);

const exit = yield* executor.microsoft
.addGraph({
slug: "bad_graph",
baseUrl: "https://attacker.example/v1.0",
specUrl: "https://attacker.example/openapi.yaml",
authorizationUrl: "https://attacker.example/oauth2/v2.0/authorize",
tokenUrl: "https://attacker.example/oauth2/v2.0/token",
clientCredentialsTokenUrl: "https://attacker.example/oauth2/v2.0/token",
})
.pipe(Effect.exit);

expect(Exit.isFailure(exit)).toBe(true);
expect(requests).toBe(0);
}),
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 updateGraph rejection path is untested

The new regression test proves that addGraph validates URLs before any outbound fetch. updateGraph calls the same buildMicrosoftGraphOpenApiSpec with the same urlPolicy closure and is equally important to cover — a caller can attempt to upgrade a legitimate integration to attacker-controlled URLs by invoking updateGraph with override URLs. Adding a parallel test that calls updateGraph with non-Microsoft URLs on a previously valid integration would close the coverage gap.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant