Widen the blank-target predicate; answer the app-secret gate before the target checks
isUsableConnectionTarget now rejects targets made only of whitespace, controls, default-ignorable code points and U+2800. The blank-target and creator_connection_required refusals move after the app-secret gate, so a caller without the secret gets 401. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
357014a43e
commit
bda14e330b
2 changed files with 23 additions and 20 deletions
|
|
@ -265,11 +265,13 @@ export function isAllowedReturnOrigin(profile: AppProfile, returnTo: string): bo
|
|||
|
||||
/**
|
||||
* A connection target names something: at least one character that is not
|
||||
* whitespace or an invisible format character (U+200B, U+FEFF and the like).
|
||||
* The one definition the start gate and the callback mint both use.
|
||||
* whitespace, a control, a default-ignorable code point (format characters,
|
||||
* variation selectors, the Hangul and Khmer fillers, the combining grapheme
|
||||
* joiner) or the Braille blank U+2800. The one definition the start gate and
|
||||
* the callback mint both use.
|
||||
*/
|
||||
export function isUsableConnectionTarget(targetId: string): boolean {
|
||||
return /[^\s\p{Cf}]/u.test(targetId);
|
||||
return /[^\s\p{Z}\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}\u2800]/u.test(targetId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
35
src/app.ts
35
src/app.ts
|
|
@ -702,23 +702,6 @@ export async function startOAuthFlow(
|
|||
};
|
||||
}
|
||||
|
||||
if (input.connection !== undefined && !isUsableConnectionTarget(input.connection.targetId)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: "invalid_connection_target", detail: "connection.targetId must name a target." },
|
||||
};
|
||||
}
|
||||
|
||||
if (callbackMint(profile, provider, input.mode, input.connection) === "refused") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: "creator_connection_required",
|
||||
detail: `${provider} on ${profile.slug} is a managed creator connection: connect needs connection.kind creator and a targetId.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const descriptor = providerCatalog[provider];
|
||||
// A provider that never mints an account attaches an identity only through
|
||||
// a bound link: the user's own access token, since the app's word does not
|
||||
|
|
@ -818,6 +801,24 @@ export async function startOAuthFlow(
|
|||
return { statusCode: 401, body: { error: "app_auth_required" } };
|
||||
}
|
||||
|
||||
// After the app-secret gate, so a caller without the secret learns nothing about what a target must look like.
|
||||
if (input.connection !== undefined && !isUsableConnectionTarget(input.connection.targetId)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: "invalid_connection_target", detail: "connection.targetId must name a target." },
|
||||
};
|
||||
}
|
||||
|
||||
if (callbackMint(profile, provider, input.mode, input.connection) === "refused") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
error: "creator_connection_required",
|
||||
detail: `${provider} on ${profile.slug} is a managed creator connection: connect needs connection.kind creator and a targetId.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const providerConfig = config.providers[provider];
|
||||
if (!providerConfig.clientId) {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue