Batch remaining hosted egress hardening fixes#1106
Conversation
Deploying with
|
| 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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 0cf12bc | Jun 23 2026, 06:17 PM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
8a56f37 to
be395f6
Compare
e023f2c to
ac0bda3
Compare
be395f6 to
94c590f
Compare
ac0bda3 to
9869c7b
Compare
94c590f to
7a69015
Compare
9869c7b to
caf9cb7
Compare
7a69015 to
e9aff2c
Compare
caf9cb7 to
64419c5
Compare
e9aff2c to
54782a5
Compare
64419c5 to
b6f0590
Compare
54782a5 to
f275c2c
Compare
b6f0590 to
0cf12bc
Compare
Greptile SummaryThis 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
Confidence Score: 4/5The 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
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"]
%%{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"]
|
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| 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); | ||
| }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
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!
What changed
Included fixes
Validation
Stack
Base:
mainSkipped: #1104