Skip to content

Commit 0c424cd

Browse files
committed
feat: add configuration for limiting external resource body limits
1 parent 1548834 commit 0c424cd

7 files changed

Lines changed: 157 additions & 2 deletions

File tree

docs/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ location / {
503503
- [extraParams](#extraparams) - Additional Authorization Request Parameters
504504
- [extraTokenClaims](#extratokenclaims) - Additional Access Token Claims
505505
- [fetch](#fetch) - Fetching External Resources
506+
- [fetchResponseBodyLimits](#fetchresponsebodylimits) - Fetch Response Body Size Limits
506507
- [issueRefreshToken](#issuerefreshtoken) - Refresh Token Issuance Policy
507508
- [loadExistingGrant](#loadexistinggrant) - Loading Existing Grants
508509
- [pairwiseIdentifier](#pairwiseidentifier) - Pairwise Subject Identifier Generation
@@ -3628,6 +3629,23 @@ _**default value**_:
36283629
36293630
---
36303631
3632+
### fetchResponseBodyLimits
3633+
3634+
Fetch Response Body Size Limits
3635+
3636+
Specifies per-purpose maximum response body size limits (in bytes) for external HTTPS resource fetches. When a limit is defined for a given purpose, the authorization server will bail out early on `Content-Length` header values exceeding the limit and will also abort reading the response body when the accumulated size exceeds the limit. Purposes with a limit of `Infinity` will not enforce any size restriction.
3637+
3638+
3639+
_**default value**_:
3640+
```js
3641+
{
3642+
jwks_uri: Infinity,
3643+
sector_identifier_uri: Infinity
3644+
}
3645+
```
3646+
3647+
---
3648+
36313649
### formats.bitsOfOpaqueRandomness
36323650
36333651
Specifies the entropy configuration for opaque token generation. The value shall be an integer (or a function returning an integer) that determines the cryptographic strength of generated opaque tokens. The resulting opaque token length shall be calculated as `Math.ceil(i / Math.log2(n))` where `i` is the specified bit count and `n` is the number of symbols in the encoding alphabet (64 characters in the base64url character set used by this implementation).

lib/helpers/configuration.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class Configuration {
7373
this.checkCibaDeliveryModes();
7474
this.checkRichAuthorizationRequests();
7575
this.checkPostMethods();
76+
this.checkFetchResponseBodyLimits();
7677

7778
delete this.cookies.long.maxAge;
7879
delete this.cookies.long.expires;
@@ -463,6 +464,18 @@ class Configuration {
463464
}
464465
}
465466

467+
checkFetchResponseBodyLimits() {
468+
const { fetchResponseBodyLimits } = this;
469+
if (!isPlainObject(fetchResponseBodyLimits)) {
470+
throw new TypeError('fetchResponseBodyLimits must be a plain object');
471+
}
472+
for (const [key, value] of Object.entries(fetchResponseBodyLimits)) {
473+
if (typeof value !== 'number' || (!Number.isSafeInteger(value) && value !== Infinity) || value < 0) {
474+
throw new TypeError(`fetchResponseBodyLimits.${JSON.stringify(key)} must be a non-negative safe integer or Infinity`);
475+
}
476+
}
477+
}
478+
466479
checkDeviceFlow() {
467480
if (this.features.deviceFlow.enabled) {
468481
if (this.features.deviceFlow.charset !== undefined) {

lib/helpers/defaults.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3417,6 +3417,25 @@ function makeDefaults() {
34173417
*/
34183418
fetch: (url, options) => globalThis.fetch(url, options),
34193419

3420+
/*
3421+
* fetchResponseBodyLimits
3422+
*
3423+
* title: Fetch Response Body Size Limits
3424+
*
3425+
* description: Specifies per-purpose maximum response body size limits (in bytes) for
3426+
* external HTTPS resource fetches. When a limit is defined for a given purpose, the
3427+
* authorization server will bail out early on `Content-Length` header values exceeding
3428+
* the limit and will also abort reading the response body when the accumulated size
3429+
* exceeds the limit. Purposes with a limit of `Infinity` will not enforce
3430+
* any size restriction.
3431+
*/
3432+
fetchResponseBodyLimits: {
3433+
// TODO: introduce default limits in v10.x
3434+
jwks_uri: Infinity,
3435+
// TODO: introduce default limits in v10.x
3436+
sector_identifier_uri: Infinity,
3437+
},
3438+
34203439
/*
34213440
* enableHttpPostMethods
34223441
*

lib/helpers/fetch_body_check.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import instance from './weak_cache.js';
2+
3+
export default async function fetchBodyCheck(provider, purpose, response) {
4+
const limit = instance(provider).configuration.fetchResponseBodyLimits[purpose];
5+
6+
if (Number.isFinite(limit)) {
7+
const contentLength = response.headers.get('content-length');
8+
if (contentLength && parseInt(contentLength, 10) > limit) {
9+
await response.body?.cancel();
10+
throw new Error('response too large');
11+
}
12+
}
13+
14+
const chunks = [];
15+
let received = 0;
16+
for await (const chunk of response.body) {
17+
received += chunk.length;
18+
if (Number.isFinite(limit) && received > limit) {
19+
await response.body?.cancel();
20+
throw new Error('response too large');
21+
}
22+
chunks.push(chunk);
23+
}
24+
return Buffer.concat(chunks);
25+
}

lib/helpers/sector_validate.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { STATUS_CODES } from 'node:http';
33
import instance from './weak_cache.js';
44
import { InvalidClientMetadata } from './errors.js';
55
import fetchRequest from './fetch_request.js';
6+
import fetchBodyCheck from './fetch_body_check.js';
67

78
export default async function sectorValidate(provider, client) {
89
if (!instance(provider).configuration.sectorIdentifierUriValidate(client)) {
@@ -24,7 +25,13 @@ export default async function sectorValidate(provider, client) {
2425

2526
let body;
2627
try {
27-
body = await response.json();
28+
body = (await fetchBodyCheck(provider, 'sector_identifier_uri', response)).toString();
29+
} catch (err) {
30+
throw new InvalidClientMetadata('could not load sector_identifier_uri response', err.message);
31+
}
32+
33+
try {
34+
body = JSON.parse(body);
2835
} catch (err) {
2936
throw new InvalidClientMetadata('failed to parse sector_identifier_uri JSON response', err.message);
3037
}

lib/models/client.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import sectorValidate from '../helpers/sector_validate.js';
2121
import addClient from '../helpers/add_client.js';
2222
import getSchema from '../helpers/client_schema.js';
2323
import fetchRequest from '../helpers/fetch_request.js';
24+
import fetchBodyCheck from '../helpers/fetch_body_check.js';
2425

2526
// intentionally ignore x5t#S256 so that they are left to be calculated by the library
2627
const EC_CURVES = new Set(['P-256', 'P-384', 'P-521']);
@@ -175,7 +176,8 @@ export default function getClient(provider) {
175176
},
176177
});
177178

178-
const body = await response.json();
179+
const buf = await fetchBodyCheck(provider, 'jwks_uri', response);
180+
const body = JSON.parse(buf.toString());
179181
const { headers, status } = response;
180182

181183
// min refetch in 60 seconds unless cache headers say a longer response ttl

test/configuration/configuration.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,75 @@ describe('Provider configuration', () => {
8686
});
8787
}).to.throw('HTTP POST Method support requires that cookies.long.sameSite is set to none');
8888
});
89+
90+
describe('fetchResponseBodyLimits', () => {
91+
it('accepts valid finite limits', () => {
92+
const conf = new Configuration({
93+
fetchResponseBodyLimits: {
94+
jwks_uri: 10240,
95+
sector_identifier_uri: 2048,
96+
},
97+
});
98+
expect(conf.fetchResponseBodyLimits.jwks_uri).to.equal(10240);
99+
});
100+
101+
it('accepts Infinity (no limit)', () => {
102+
const conf = new Configuration({
103+
fetchResponseBodyLimits: {
104+
jwks_uri: Infinity,
105+
},
106+
});
107+
expect(conf.fetchResponseBodyLimits.jwks_uri).to.equal(Infinity);
108+
});
109+
110+
it('rejects negative values', () => {
111+
expect(() => {
112+
new Configuration({ // eslint-disable-line no-new
113+
fetchResponseBodyLimits: {
114+
jwks_uri: -1,
115+
},
116+
});
117+
}).to.throw('fetchResponseBodyLimits."jwks_uri" must be a non-negative safe integer or Infinity');
118+
});
119+
120+
it('rejects non-number values', () => {
121+
expect(() => {
122+
new Configuration({ // eslint-disable-line no-new
123+
fetchResponseBodyLimits: {
124+
jwks_uri: '1024',
125+
},
126+
});
127+
}).to.throw('fetchResponseBodyLimits."jwks_uri" must be a non-negative safe integer or Infinity');
128+
});
129+
130+
it('rejects null values', () => {
131+
expect(() => {
132+
new Configuration({ // eslint-disable-line no-new
133+
fetchResponseBodyLimits: {
134+
jwks_uri: null,
135+
},
136+
});
137+
}).to.throw('fetchResponseBodyLimits."jwks_uri" must be a non-negative safe integer or Infinity');
138+
});
139+
140+
it('rejects NaN', () => {
141+
expect(() => {
142+
new Configuration({ // eslint-disable-line no-new
143+
fetchResponseBodyLimits: {
144+
jwks_uri: NaN,
145+
},
146+
});
147+
}).to.throw('fetchResponseBodyLimits."jwks_uri" must be a non-negative safe integer or Infinity');
148+
});
149+
150+
it('rejects floats', () => {
151+
expect(() => {
152+
new Configuration({ // eslint-disable-line no-new
153+
fetchResponseBodyLimits: {
154+
jwks_uri: 10.5,
155+
},
156+
});
157+
}).to.throw('fetchResponseBodyLimits."jwks_uri" must be a non-negative safe integer or Infinity');
158+
});
159+
});
89160
});

0 commit comments

Comments
 (0)