fix(oauth): accept exact Microsoft substrate ChatHub host (#16327) (#17137)

Merged via /merge-batch (2026-10-19). Board validation on the current base together with #17138: npm run lint 0, typecheck core + open-sse 0, check:docs-all 0, file-size, test-masking, changelog-integrity, env-doc-sync, complexity, cognitive, provider-consistency 0; 187 affected unit files 2093 pass with only the 3 base-red #15306 i18n state tests failing. CI reds are inherited (i18n state tests, Docs Sync drift, Gate / Quality, E2E 3/9 mobile sidebar fail on unrelated PRs too).
This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-10-19 08:02:41 -03:00 • committed by GitHub
parent 03be754e95
commit aae8749eed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 19 additions and 3 deletions

View file

@ -0,0 +1 @@
- Accept the exact Microsoft 365 Copilot ChatHub host `substrate.svc.cloud.microsoft` when importing or pasting credentials, preserving the host through validation and execution while rejecting untrusted hosts and malformed URLs (#16327).

View file

@ -7,7 +7,7 @@ export interface M365CredentialParts {
const CHATHUB_PATH = "/m365Copilot/Chathub/";
const TRUSTED_HOST =
/^(?:[a-z0-9-]+\.)*(?:m365\.cloud\.microsoft|copilot\.microsoft\.com|substrate\.office\.com)$/i;
/^(?:(?:[a-z0-9-]+\.)*(?:m365\.cloud\.microsoft|copilot\.microsoft\.com|substrate\.office\.com)|substrate\.svc\.cloud\.microsoft)$/i;
export type M365ChatHubUrlResult =
| { credential: Required<M365CredentialParts> }

View file

@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { validateCopilotM365WebProvider } from "../../src/lib/providers/validation/webProvidersB.ts";
import { extractM365CredentialFromHar } from "../../src/shared/utils/m365HarImport.ts";
import {
resolveConnectionParams,
@ -18,12 +19,15 @@ describe("M365 HAR host compatibility (#16327)", () => {
"eu.m365.cloud.microsoft",
"copilot.microsoft.com",
"substrate.office.com",
"substrate.svc.cloud.microsoft",
"SUBSTRATE.SVC.CLOUD.MICROSOFT:443",
"M365.CLOUD.MICROSOFT:443",
]) {
it(`preserves ${host} from HAR through execution`, () => {
it(`preserves ${host} from HAR through validation and execution`, async () => {
const imported = extractM365CredentialFromHar(har(url(host)));
assert.equal(imported.ok, true);
if (!imported.ok) return;
assert.equal((await validateCopilotM365WebProvider({ apiKey: imported.apiKey })).valid, true);
const params = resolveConnectionParams({ apiKey: imported.apiKey });
assert.ok(!("error" in params));
assert.equal(params.host, host.toLowerCase().replace(":443", ""));
@ -59,6 +63,16 @@ describe("M365 HAR host compatibility (#16327)", () => {
for (const unsafe of [
url("m365.cloud.microsoft.attacker.invalid"),
url("attacker.invalid"),
url("substrate.svc.cloud.microsoft.attacker.invalid"),
url("sub.substrate.svc.cloud.microsoft"),
url("other.svc.cloud.microsoft"),
url("user@substrate.svc.cloud.microsoft"),
url("substrate.svc.cloud.microsoft:8443"),
url("substrate.svc.cloud.microsoft").replace("wss:", "ws:"),
url("substrate.svc.cloud.microsoft").replace("wss:", "https:"),
url("substrate.svc.cloud.microsoft").replace("Chathub", "StreamHub"),
url("substrate.svc.cloud.microsoft").replace(path, "user@tenant/extra"),
url("substrate.svc.cloud.microsoft").replace(path, "%ZZ"),
url("m365.cloud.microsoft:8443"),
url("user@m365.cloud.microsoft"),
url("m365.cloud.microsoft").replace("wss:", "ws:"),
@ -66,9 +80,10 @@ describe("M365 HAR host compatibility (#16327)", () => {
url("substrate.office.com").replace(path, "user@tenant%3Bhost=attacker.invalid"),
url("substrate.office.com").replace(path, "user@tenant%3Faccess_token=other"),
]) {
it(`rejects unsafe HAR URL ${unsafe.replace(token, "REDACTED")}`, () => {
it(`rejects unsafe HAR URL ${unsafe.replace(token, "REDACTED")}`, async () => {
const imported = extractM365CredentialFromHar(har(unsafe));
assert.equal(imported.ok, false);
assert.equal((await validateCopilotM365WebProvider({ apiKey: unsafe })).valid, false);
const params = resolveConnectionParams({ apiKey: unsafe });
assert.ok("error" in params);
if ("error" in params) assert.ok(!params.error.includes(token));