Skip to content

Commit 36ed9d8

Browse files
committed
Ask Claude to validate client registrations better.
prompt: In `handleClientRegistration()`, we accept a JSON request from a possibly-untrusted client, which we then store long-term. We should validate the type of every part of the data that we end up storing, so that our storage cannot end up with unexpected content. We should also validate that the overall input is not excessively large; a size limit of 1MiB should be sufficient. Claude went a little overboard at first. prompt: This is more validation than we need. I think all we really need to enforce is that string fields are strings, and array-of-strings fields are arrays of strings. No need to check specific array lengths or that values are within an allowed set. prompt: Better, but: (1) Please narrow the try/catch to wrap only the initialization of clientInfo, where all the validation is done. We don't want to catch exceptions that might be thrown by things like generateRandomString as they may be unrelated errors. (2) There's no need for the validation functions to check for `null` and `undefined` explicitly since those will fail the subsequent type checks anyway. (I actually misunderstood point 2, Claude was intentionally allowing null/undefined values since almost everything in this metadata is optional, but it cheerfully told me I was right, removed the null check but kept the undefined check...) prompt: Almost there, but why manually validate `redirect_uris`? Can't we just use `validateStringArray()`?
1 parent acbbadb commit 36ed9d8

File tree

1 file changed

+84
-22
lines changed

1 file changed

+84
-22
lines changed

oauth-provider.ts

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ class OAuthProviderImpl {
822822
let contentType = request.headers.get('Content-Type') || '';
823823
let body: any = {};
824824

825-
// According to OAuth 2.0 RFC 6749 Section 2.3, token requests MUST use
825+
// According to OAuth 2.0 RFC 6749 Section 2.3, token requests MUST use
826826
// application/x-www-form-urlencoded content type
827827
if (!contentType.includes('application/x-www-form-urlencoded')) {
828828
return createErrorResponse(
@@ -1264,39 +1264,101 @@ class OAuthProviderImpl {
12641264
);
12651265
}
12661266

1267-
// Parse client metadata
1268-
const clientMetadata = await request.json();
1267+
// Check content length to ensure it's not too large (1 MiB limit)
1268+
const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
1269+
if (contentLength > 1048576) { // 1 MiB = 1048576 bytes
1270+
return createErrorResponse(
1271+
'invalid_request',
1272+
'Request payload too large, must be under 1 MiB',
1273+
413
1274+
);
1275+
}
12691276

1270-
// Validate redirect URIs
1271-
if (!clientMetadata.redirect_uris || !Array.isArray(clientMetadata.redirect_uris) || clientMetadata.redirect_uris.length === 0) {
1277+
// Parse client metadata with a size limitation
1278+
let clientMetadata;
1279+
try {
1280+
const text = await request.text();
1281+
if (text.length > 1048576) { // Double-check text length
1282+
return createErrorResponse(
1283+
'invalid_request',
1284+
'Request payload too large, must be under 1 MiB',
1285+
413
1286+
);
1287+
}
1288+
clientMetadata = JSON.parse(text);
1289+
} catch (error) {
12721290
return createErrorResponse(
1273-
'invalid_redirect_uri',
1274-
'At least one redirect URI is required'
1291+
'invalid_request',
1292+
'Invalid JSON payload',
1293+
400
12751294
);
12761295
}
12771296

1297+
// Basic type validation functions
1298+
const validateStringField = (field: any): string | undefined => {
1299+
if (field === undefined) {
1300+
return undefined;
1301+
}
1302+
if (typeof field !== 'string') {
1303+
throw new Error('Field must be a string');
1304+
}
1305+
return field;
1306+
};
1307+
1308+
const validateStringArray = (arr: any): string[] | undefined => {
1309+
if (arr === undefined) {
1310+
return undefined;
1311+
}
1312+
if (!Array.isArray(arr)) {
1313+
throw new Error('Field must be an array');
1314+
}
1315+
1316+
// Validate all elements are strings
1317+
for (const item of arr) {
1318+
if (typeof item !== 'string') {
1319+
throw new Error('All array elements must be strings');
1320+
}
1321+
}
1322+
1323+
return arr;
1324+
};
1325+
12781326
// Create client
12791327
const clientId = generateRandomString(16);
12801328
const clientSecret = generateRandomString(32);
12811329

12821330
// Hash the client secret before storing
12831331
const hashedSecret = await hashSecret(clientSecret);
12841332

1285-
const clientInfo: ClientInfo = {
1286-
clientId,
1287-
clientSecret: hashedSecret, // Store the hashed secret
1288-
redirectUris: clientMetadata.redirect_uris,
1289-
clientName: clientMetadata.client_name,
1290-
logoUri: clientMetadata.logo_uri,
1291-
clientUri: clientMetadata.client_uri,
1292-
policyUri: clientMetadata.policy_uri,
1293-
tosUri: clientMetadata.tos_uri,
1294-
jwksUri: clientMetadata.jwks_uri,
1295-
contacts: clientMetadata.contacts,
1296-
grantTypes: clientMetadata.grant_types || ['authorization_code', 'refresh_token'],
1297-
responseTypes: clientMetadata.response_types || ['code'],
1298-
registrationDate: Math.floor(Date.now() / 1000)
1299-
};
1333+
let clientInfo: ClientInfo;
1334+
try {
1335+
// Validate redirect URIs - must exist and have at least one entry
1336+
const redirectUris = validateStringArray(clientMetadata.redirect_uris);
1337+
if (!redirectUris || redirectUris.length === 0) {
1338+
throw new Error('At least one redirect URI is required');
1339+
}
1340+
1341+
clientInfo = {
1342+
clientId,
1343+
clientSecret: hashedSecret,
1344+
redirectUris,
1345+
clientName: validateStringField(clientMetadata.client_name),
1346+
logoUri: validateStringField(clientMetadata.logo_uri),
1347+
clientUri: validateStringField(clientMetadata.client_uri),
1348+
policyUri: validateStringField(clientMetadata.policy_uri),
1349+
tosUri: validateStringField(clientMetadata.tos_uri),
1350+
jwksUri: validateStringField(clientMetadata.jwks_uri),
1351+
contacts: validateStringArray(clientMetadata.contacts),
1352+
grantTypes: validateStringArray(clientMetadata.grant_types) || ['authorization_code', 'refresh_token'],
1353+
responseTypes: validateStringArray(clientMetadata.response_types) || ['code'],
1354+
registrationDate: Math.floor(Date.now() / 1000)
1355+
};
1356+
} catch (error) {
1357+
return createErrorResponse(
1358+
'invalid_client_metadata',
1359+
error instanceof Error ? error.message : 'Invalid client metadata'
1360+
);
1361+
}
13001362

13011363
// Store client info
13021364
await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo));

0 commit comments

Comments
 (0)