@stonyx/rest-server 0.2.1-beta.92 → 0.2.1-beta.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -23
- package/config/environment.js +69 -0
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +41 -1
- package/dist/request.js.map +1 -1
- package/dist/route-matching.d.ts +118 -6
- package/dist/route-matching.d.ts.map +1 -1
- package/dist/route-matching.js +158 -6
- package/dist/route-matching.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,24 +82,34 @@ Configuration is read from `stonyx/config` under `restServer`:
|
|
|
82
82
|
| `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**. |
|
|
83
83
|
| `strictRoutes` | **Boolean** | `true` | Match route paths strictly, so a trailing slash does not match a route registered without one. Disable via `REST_STRICT_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note `GET /health/` now 404s. |
|
|
84
84
|
| `canonicalRoutes` | **Boolean** | `true` | Reject a request whose raw target is not the canonical path express matched, before your `auth` hook runs. Disable via `REST_CANONICAL_ROUTES=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note `GET /route/` at a mount root and every absolute-form request target now 404 on the routes this module registers (see the scope limit under [Upgrading](#upgrading-behaviour-changes)). |
|
|
85
|
+
| `canonicalEncoding` | **Boolean** | `true` | Reject a request whose raw target percent-encodes an RFC 3986 §2.3 *unreserved* character (`A-Z a-z 0-9 - . _ ~`), before your `auth` hook runs. Disable via `REST_CANONICAL_ENCODING=false`. See [Route Matching Strictness](#route-matching-strictness) — **disabling this re-opens a security hole**, and note that a client over-encoding an unreserved character in a path now gets 404 (see [Upgrading](#upgrading-behaviour-changes)). Like `canonicalRoutes` this is a **registration-site** control, not a global one: the check runs in the handlers mounted from your request classes, so a route registered directly on `RestServer.instance.api` gets none of it — measured, `GET /direct/%73ecret` → **200** with `id "secret"` while `GET /enc/%73ecret` → 404. |
|
|
85
86
|
| `trustProxy` | **Boolean** | `false` | Trust reverse proxy headers (e.g. `X-Forwarded-Proto`). Enable via `REST_TRUST_PROXY=true` when running behind a load balancer such as AWS ALB/ELB to ensure correct protocol detection. |
|
|
86
87
|
| `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
|
|
87
88
|
|
|
88
89
|
### Route Matching Strictness
|
|
89
90
|
|
|
90
|
-
Routes match **case-sensitively, strictly, and only at their canonical target
|
|
91
|
-
by default
|
|
91
|
+
Routes match **case-sensitively, strictly, and only at their canonical target**
|
|
92
|
+
by default, and a raw target that percent-encodes an RFC 3986 §2.3 *unreserved*
|
|
93
|
+
character is rejected. Four controls, all on.
|
|
94
|
+
|
|
95
|
+
Note what that does **not** say. It is not "one accepted spelling per id", and
|
|
96
|
+
this module cannot give you that: reserved characters, non-ASCII bytes and
|
|
97
|
+
control octets all stay encodable, and every one of them whose hex carries a
|
|
98
|
+
letter digit aliases by hex-digit case. The residual is stated and measured
|
|
99
|
+
below, under [the residual](#the-residual-stated-plainly).
|
|
92
100
|
|
|
93
101
|
| axis | control | example that no longer matches |
|
|
94
102
|
|---|---|---|
|
|
95
103
|
| casing | `case sensitive routing` (setting) | `GET /users/Success` -> does not reach `/success` |
|
|
96
104
|
| trailing slash | `strict routing` (setting) | `GET /users/success/` -> does not reach `/success` |
|
|
97
105
|
| canonical target | `canonicalRoutes` (per-request check) | `GET /users/` and `GET http://host/users` -> do not reach the mounted `/users` class |
|
|
106
|
+
| percent-encoding | `canonicalEncoding` (per-request check) | `GET /users/%73ecret` -> does not reach `/users/:id` with `id === "secret"` |
|
|
98
107
|
|
|
99
|
-
The first two are express settings applied at both construction sites. The
|
|
100
|
-
|
|
101
|
-
per-request
|
|
102
|
-
matched,
|
|
108
|
+
The first two are express settings applied at both construction sites. The last
|
|
109
|
+
two are **not settings** — no express setting can express either — they are
|
|
110
|
+
per-request checks run ahead of your `auth` hook: one compares the raw request
|
|
111
|
+
target against the path express matched, the other rejects a raw target that
|
|
112
|
+
percent-encodes a character which never needs encoding. See
|
|
103
113
|
[`src/route-matching.ts`](src/route-matching.ts).
|
|
104
114
|
|
|
105
115
|
Read [What this does not do](#what-this-does-not-do) and
|
|
@@ -191,19 +201,100 @@ Routes registered *with* a literal trailing slash are unaffected — their
|
|
|
191
201
|
canonical target carries the slash. So are index-mounted route classes and
|
|
192
202
|
query strings on canonical paths.
|
|
193
203
|
|
|
194
|
-
**Param routes: "unaffected" means "no regression"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
`
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
**Param routes: "unaffected" means "no regression".** `/resource/:id` keeps
|
|
205
|
+
matching exactly as it did. `canonicalRoutes` is structurally blind to how a
|
|
206
|
+
param value is *spelled* — express decodes only `req.params`, so `target` and
|
|
207
|
+
`canonical` are both the same encoded string and this comparison passes the
|
|
208
|
+
request through by construction. That axis has its own control, below.
|
|
209
|
+
|
|
210
|
+
#### The percent-encoding check (`canonicalEncoding`)
|
|
211
|
+
|
|
212
|
+
Express decodes **`req.params` and nothing else**. `req.path` and
|
|
213
|
+
`req.originalUrl` both stay percent-encoded, so an `auth` hook comparing either
|
|
214
|
+
of them against a fixed string was walked past by re-spelling the id:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
before after
|
|
218
|
+
GET /enc/secret 401 401 (hook fires, request blocked)
|
|
219
|
+
GET /enc/%73ecret 200 404 (hook never fired; handler got id "secret")
|
|
220
|
+
GET /enc/%73%65%63%72%65%74 200 404
|
|
221
|
+
GET /private/%66ailure 200 404 (guard missed; absorbed by a sibling /:id)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`canonicalEncoding` closes it
|
|
225
|
+
([#56](https://github.com/abofs/stonyx-rest-server/issues/56)). Before your
|
|
226
|
+
`auth` hook runs, the query-stripped raw target is rejected as a plain 404 if it
|
|
227
|
+
contains a percent-triplet whose octet is an
|
|
228
|
+
[RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3)
|
|
229
|
+
**unreserved** character — `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`. Those are
|
|
230
|
+
exactly the characters a URI generator must **not** encode and a normalizer
|
|
231
|
+
**must** decode, so nothing a client is required to send is affected.
|
|
232
|
+
|
|
233
|
+
**This is not a list of spellings, it is a family.** For an id of *n* bytes
|
|
234
|
+
there are `∏(1 + vᵢ) − 1` non-canonical spellings, where a byte whose hex
|
|
235
|
+
carries a letter digit has two (`m`, `%6d`, `%6D`). Measured against unfixed
|
|
236
|
+
code: **63 of 63** spellings of `secret` returned 200, and **71 of 71** of
|
|
237
|
+
`admin`. Enumerating them in your own hook is not a remedy.
|
|
238
|
+
|
|
239
|
+
**Three things it deliberately does not reject:**
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
GET /enc/sec%2fret -> 200 id "sec/ret" %2f is RESERVED — must stay encodable
|
|
243
|
+
GET /enc/a%2Bb -> 200 id "a+b" %2B is RESERVED
|
|
244
|
+
GET /enc/%2573ecret -> 200 id "%73ecret" express decodes exactly ONCE
|
|
245
|
+
GET /enc/x?name=%61 -> 200 id "x" the query string is stripped, not scanned
|
|
246
|
+
GET /enc/%zz -> 400 malformed escapes are the router's 400, unchanged
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
If you were tempted to write `decodeURIComponent(req.path)` in your hook: the
|
|
250
|
+
first line is why not. The router **splits then decodes**, so `sec%2fret` is one
|
|
251
|
+
segment naming the id `sec/ret`; a hook that decodes then splits sees two
|
|
252
|
+
segments and denies a request the router routed somewhere else entirely. And a
|
|
253
|
+
hook that decodes *until stable* denies line three, which is a legitimately
|
|
254
|
+
distinct id.
|
|
255
|
+
|
|
256
|
+
#### The residual, stated plainly
|
|
257
|
+
|
|
258
|
+
**This does not give each id one spelling.** The rule rejects an over-encoded
|
|
259
|
+
*unreserved* octet, which means every octet **outside** `A-Za-z0-9-._~` stays
|
|
260
|
+
encodable — and any such octet whose hex carries a letter digit therefore has
|
|
261
|
+
two accepted spellings, upper- and lower-case hex. **That is every reserved
|
|
262
|
+
character, every non-ASCII byte, and every control octet whose hex carries a
|
|
263
|
+
letter digit — not only the reserved ones.** It is not the whole complement of
|
|
264
|
+
`A-Za-z0-9-._~`: `%21` and `%40` carry no letter hex digit and alias
|
|
265
|
+
literal-versus-encoded instead, `%00` and `%09` keep exactly one accepted
|
|
266
|
+
spelling and do not alias at all, and `%90` is a 400. Two different raw targets
|
|
267
|
+
can still name the same record:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
GET /enc/a+b -> 200 id "a+b"
|
|
271
|
+
GET /enc/a%2Bb -> 200 id "a+b" <- two accepted spellings, one id
|
|
272
|
+
GET /enc/sec%2fret -> 200 id "sec/ret"
|
|
273
|
+
GET /enc/sec%2Fret -> 200 id "sec/ret" <- hex-digit case, same id again
|
|
274
|
+
|
|
275
|
+
GET /i18n/caf%C3%A9 -> 401 hook denies this spelling
|
|
276
|
+
GET /i18n/caf%c3%a9 -> 200 id "café" <- same id, hook walked past
|
|
277
|
+
GET /i18n/%E5%8C%97%E4%BA%AC -> 401
|
|
278
|
+
GET /i18n/%e5%8c%97%e4%ba%ac -> 200 id "北京"
|
|
279
|
+
GET /i18n/a%0Db -> 401
|
|
280
|
+
GET /i18n/a%0db -> 200 id "a\rb" <- a CONTROL octet, same aliasing
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The last six lines were measured against this module's shipped predicate on a
|
|
284
|
+
deny list holding **no reserved character at all** — the three uppercase-hex
|
|
285
|
+
spellings, nothing else. If your ids are i18n text, or anything else that is not
|
|
286
|
+
pure `A-Za-z0-9-._~`, the residual applies to you. Reading it as "only affects
|
|
287
|
+
ids with a `/` or a `+` in them" is the mistake this paragraph exists to
|
|
288
|
+
prevent.
|
|
289
|
+
|
|
290
|
+
**So a hook comparing a raw path string is still unsound for any id carrying an
|
|
291
|
+
octet outside `A-Za-z0-9-._~` that keeps more than one accepted spelling —
|
|
292
|
+
reserved characters, non-ASCII bytes and control octets whose hex carries a
|
|
293
|
+
letter digit alike — and `req.params` is the sound comparison.** `req.params` is
|
|
294
|
+
decoded by express and is populated *before* your `auth` hook runs, by design —
|
|
295
|
+
compare that, and none of this applies to you. This module cannot close the
|
|
296
|
+
residual for you without 404ing encodings clients are entitled to send; see
|
|
297
|
+
[Consumer Contracts](#consumer-contracts).
|
|
207
298
|
|
|
208
299
|
#### What this does not do
|
|
209
300
|
|
|
@@ -234,9 +325,22 @@ another variant of the bug above.
|
|
|
234
325
|
|
|
235
326
|
#### Upgrading: behaviour changes
|
|
236
327
|
|
|
237
|
-
All
|
|
328
|
+
All four controls change which requests match, so all four are
|
|
238
329
|
consumer-visible.
|
|
239
330
|
|
|
331
|
+
**A client that over-encodes an unreserved character in a path now gets 404.**
|
|
332
|
+
`GET /public/url-params/%61/b/c` returns **404** where it previously returned
|
|
333
|
+
200 — measured on this repo's own fixture. `%61` is `a`, and
|
|
334
|
+
[RFC 3986 §2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3) says a
|
|
335
|
+
generator must not encode it, so no correct client emits this. Some do anyway:
|
|
336
|
+
over-eager `encodeURIComponent` on an id that never needed it, a URL builder
|
|
337
|
+
that percent-encodes everything, or a client library normalizing in the wrong
|
|
338
|
+
direction. Reserved characters are **unaffected** — `%2f`, `%2B`, `%25` and
|
|
339
|
+
every non-ASCII byte still route, and so does anything in the query string.
|
|
340
|
+
Remediation is `REST_CANONICAL_ENCODING=false`, or fix the client. Like the two
|
|
341
|
+
below, the rejection is **indistinguishable from a route that was never
|
|
342
|
+
registered**, and this module emits no request logging.
|
|
343
|
+
|
|
240
344
|
**Clients or forward proxies sending absolute-form request targets now get 404
|
|
241
345
|
on every route mounted from a request class.** `GET http://host/admin HTTP/1.1`
|
|
242
346
|
is a legal request target
|
|
@@ -289,16 +393,17 @@ no log line and no stack, so it looks like a deploy that dropped a route.
|
|
|
289
393
|
|
|
290
394
|
#### Opting out
|
|
291
395
|
|
|
292
|
-
|
|
396
|
+
Four separate flags, one per axis:
|
|
293
397
|
|
|
294
398
|
```bash
|
|
295
399
|
REST_CASE_SENSITIVE_ROUTES=false # restores case-insensitive matching (#47)
|
|
296
400
|
REST_STRICT_ROUTES=false # restores trailing-slash tolerance (#50)
|
|
297
401
|
REST_CANONICAL_ROUTES=false # restores non-canonical request targets (#54)
|
|
402
|
+
REST_CANONICAL_ENCODING=false # restores percent-encoded spellings (#56)
|
|
298
403
|
```
|
|
299
404
|
|
|
300
405
|
or equivalently
|
|
301
|
-
`restServer: { caseSensitiveRoutes: false, strictRoutes: false, canonicalRoutes: false }`.
|
|
406
|
+
`restServer: { caseSensitiveRoutes: false, strictRoutes: false, canonicalRoutes: false, canonicalEncoding: false }`.
|
|
302
407
|
|
|
303
408
|
**They are deliberately separate keys, and none implies the others.** Slash
|
|
304
409
|
tolerance is a legitimate need — a health-check URL you cannot change today is
|
|
@@ -315,6 +420,18 @@ slash *and* the absolute-form target — against any hook authorizing on
|
|
|
315
420
|
`req.originalUrl`. It is env-only, so restoring service does not need a
|
|
316
421
|
redeploy; use it to stop the bleeding, then fix the client and remove it.
|
|
317
422
|
|
|
423
|
+
**`REST_CANONICAL_ENCODING=false` re-opens the `#56` bypass, and it is the
|
|
424
|
+
widest of the four.** With it set, `GET /users/%73ecret` reaches your `/:id`
|
|
425
|
+
handler with `id === "secret"` while your hook compared `%73ecret` and did not
|
|
426
|
+
match — and it does that against a hook comparing `req.path` **or**
|
|
427
|
+
`req.originalUrl`, on every route class with a param segment. The other three
|
|
428
|
+
flags each re-open one field's worth of exposure; this one re-opens both. It is
|
|
429
|
+
also **independent** of `REST_CANONICAL_ROUTES`: if you have to set that one for
|
|
430
|
+
an absolute-form-emitting forward proxy, you keep this one on, which is exactly
|
|
431
|
+
why they are separate keys. If you must set it, the mitigation that costs you
|
|
432
|
+
nothing is to compare `req.params` in your hook rather than a raw path string —
|
|
433
|
+
`req.params` is decoded and was never exposed to this.
|
|
434
|
+
|
|
318
435
|
**Each flag restores the corresponding vulnerability described above** — the
|
|
319
436
|
URL-based authorization in your application becomes bypassable along that axis
|
|
320
437
|
again. They exist as one-line remediations for an existing deployment, not as a
|
|
@@ -331,7 +448,8 @@ rather than left implied by the sections above.
|
|
|
331
448
|
| you must | because | symptom if you don't |
|
|
332
449
|
|---|---|---|
|
|
333
450
|
| **Strip the query string** before comparing `req.originalUrl` to a fixed path in an `auth` hook | `canonicalRoutes` compares the query-*stripped* target — a query string is a legitimately variable part of a request target, and rejecting on it would 404 every `?`-carrying request | `GET /admin?x=1` reaches your guarded handler **unauthenticated**, 200, no error, no log. Measured identical before and after `canonicalRoutes` |
|
|
334
|
-
| **Compare
|
|
451
|
+
| **Compare `req.params`, not a raw path string** | express decodes only `req.params`; `req.path` and `req.originalUrl` both stay percent-encoded. `canonicalEncoding` (#56) rejects an over-encoded *unreserved* character, but every octet outside `A-Za-z0-9-._~` stays encodable — **reserved characters, non-ASCII bytes and control octets alike** — and every one of them whose hex carries a letter digit aliases by hex-digit case, so one decoded id still has more than one accepted spelling: `GET /orders/a+b` and `GET /orders/a%2Bb` both run the handler with `id === "a+b"`, `sec%2fret` / `sec%2Fret` both give `sec/ret`, and `caf%C3%A9` / `caf%c3%a9` both give `café`. The class is **not** limited to reserved characters — see [the residual](#the-residual-stated-plainly) | your hook compares one spelling, the request arrives in another, and the handler runs **unauthenticated** — 200, no error, no log. Comparing `req.params.id` instead is immune by construction, and it is populated before `auth()` runs |
|
|
452
|
+
| **Compare param values with the same casing you look them up with** | param *values* are never case-normalized, and record ids are legitimately case-sensitive | `GET /orders/SECRET` runs the handler with `id === "SECRET"` while your hook compared `secret`. Note that lower-casing the value is **not** the fix: it false-denies a genuinely distinct `SECRET` record and, measured in a sibling module, false-allowed an encoded spelling at the same time |
|
|
335
453
|
| **Return `undefined` from `auth()` to mean "authorized"** — never `0` | any integer return is sent as the HTTP status | returning `0` sends a `0` status rather than allowing the request |
|
|
336
454
|
|
|
337
455
|
`test/sample/requests/admin.ts` in this repo is the worked example of a
|
package/config/environment.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const {
|
|
2
|
+
REST_CANONICAL_ENCODING,
|
|
2
3
|
REST_CANONICAL_ROUTES,
|
|
3
4
|
REST_CASE_SENSITIVE_ROUTES,
|
|
4
5
|
REST_CORS_ORIGIN,
|
|
@@ -131,6 +132,74 @@ const config = {
|
|
|
131
132
|
// keep a live assertion on this default.
|
|
132
133
|
canonicalRoutes: REST_CANONICAL_ROUTES !== 'false',
|
|
133
134
|
|
|
135
|
+
// Secure by default, same polarity and same reasoning as the three keys
|
|
136
|
+
// above: a request whose RAW target percent-encodes an RFC 3986 2.3
|
|
137
|
+
// UNRESERVED character (ALPHA / DIGIT / "-" / "." / "_" / "~") is rejected
|
|
138
|
+
// with a plain 404 before the consumer's `auth` hook runs
|
|
139
|
+
// (abofs/stonyx-rest-server#56). Like canonicalRoutes this is NOT an express
|
|
140
|
+
// setting -- it is a per-request check in src/route-matching.ts
|
|
141
|
+
// (`shouldRejectEncoding`), called from the handler closure in
|
|
142
|
+
// src/request.ts.
|
|
143
|
+
//
|
|
144
|
+
// What it closes: express decodes `req.params` and NOTHING else, so a
|
|
145
|
+
// consumer hook comparing `req.path` OR `req.originalUrl` was walked past by
|
|
146
|
+
// re-spelling an id -- `GET /enc/secret` -> 401 while
|
|
147
|
+
// `GET /enc/%73ecret` -> 200 with the guarded handler running
|
|
148
|
+
// unauthenticated and `req.params.id === 'secret'`. The spelling family is
|
|
149
|
+
// PROD(1 + v_i) - 1 per id, measured at 63 spellings for `secret` and 71 for
|
|
150
|
+
// `admin`, ALL of them 200 before this key existed. It is EXCLUSIVE to route
|
|
151
|
+
// classes carrying a `:param` segment; literal routes and mount segments
|
|
152
|
+
// match raw and were never reachable this way.
|
|
153
|
+
//
|
|
154
|
+
// BEHAVIOUR CHANGE for consumers upgrading, on a FOURTH axis:
|
|
155
|
+
// Any client that over-encodes an unreserved character in a path now gets
|
|
156
|
+
// 404. Measured on this repo's own fixture:
|
|
157
|
+
// `GET /public/url-params/%61/b/c` -> 404 (was 200). Over-encoding an
|
|
158
|
+
// unreserved character is never required by RFC 3986 -- a normaliser MUST
|
|
159
|
+
// decode these (6.2.2.2) -- so the blast radius is smaller than #54's,
|
|
160
|
+
// whose absolute-form vector hits a real deployment shape. It is still a
|
|
161
|
+
// breaking change and it is documented as one in the README.
|
|
162
|
+
//
|
|
163
|
+
// Opt out with REST_CANONICAL_ENCODING=false only as a temporary remediation
|
|
164
|
+
// -- it RE-OPENS the bypass. SEPARATE key from REST_CANONICAL_ROUTES on
|
|
165
|
+
// purpose, and this one is not a symmetry argument but a measurement: with
|
|
166
|
+
// the rule gated on `canonicalRoutes` instead of its own key,
|
|
167
|
+
// `REST_CANONICAL_ROUTES=false` returns `GET /enc/%73ecret` to 200 -- and
|
|
168
|
+
// that flag is exactly what a consumer behind an absolute-form-emitting
|
|
169
|
+
// forward proxy must set to stay up. Folding the two would hand precisely
|
|
170
|
+
// those consumers the encoding bypass as the price. Killed by
|
|
171
|
+
// test/unit/request-test.ts AC5.
|
|
172
|
+
//
|
|
173
|
+
// DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
|
|
174
|
+
// as part of abofs/stonyx-rest-server#43. Both halves RE-MEASURED for this
|
|
175
|
+
// key rather than inferred from the three above, against the 41-test suite
|
|
176
|
+
// at #56's head:
|
|
177
|
+
// (a) invert this line to `=== 'true'` ALONE, unpinned:
|
|
178
|
+
// 36 pass / 5 fail -- #56's integration AC1 and AC2, unit AC5 and AC6,
|
|
179
|
+
// and [Unit] Config AC7. It fails LOUDLY, so there is no false green.
|
|
180
|
+
// (b) pin `canonicalEncoding: true` in test/config/environment.ts AND
|
|
181
|
+
// invert this line: 40 pass / 1 fail, and the ONE failure is
|
|
182
|
+
// [Unit] Config AC7 -- every behavioural assertion goes green because
|
|
183
|
+
// the pin supplies the secure value the suite then observes. Measured
|
|
184
|
+
// again with test/unit/config-test.ts removed: 40 pass / 0 fail, a
|
|
185
|
+
// fully green suite shipping an insecure default. That is the trap, and
|
|
186
|
+
// AC7 is the only thing standing between this key and it.
|
|
187
|
+
//
|
|
188
|
+
// Conversely, weakening the READ in src/route-matching.ts to `=== true`
|
|
189
|
+
// reports 40 pass / 1 fail with unit AC6 as the only failure and every
|
|
190
|
+
// integration assertion green -- the two guard different halves and neither
|
|
191
|
+
// subsumes the other. Closing #43 for any of the four keys needs the
|
|
192
|
+
// subprocess-based env isolation this repo does not have; any fix must keep a
|
|
193
|
+
// live assertion on this default.
|
|
194
|
+
//
|
|
195
|
+
// FOUR security-relevant keys now, all defaulting on, all disable-able, none
|
|
196
|
+
// pinned. Per docs/framework/testing.md the pinned set has to be evaluated as
|
|
197
|
+
// a SET rather than key by key; [Unit] Config AC7 asserts all four together
|
|
198
|
+
// for that reason. Whoever takes #43 inherits four keys and this paragraph as
|
|
199
|
+
// the reason they are unpinned, rather than finding four and assuming
|
|
200
|
+
// neglect.
|
|
201
|
+
canonicalEncoding: REST_CANONICAL_ENCODING !== 'false',
|
|
202
|
+
|
|
134
203
|
enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
|
|
135
204
|
origin: REST_CORS_ORIGIN ?? '*',
|
|
136
205
|
methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
|
package/dist/request.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAqB,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAOrI,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IA4B3B,aAAa,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAqB,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAOrI,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IA4B3B,aAAa,IAAI,IAAI;CAoItB"}
|
package/dist/request.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import config from 'stonyx/config';
|
|
3
3
|
import { makeArray } from '@stonyx/utils/object';
|
|
4
|
-
import applyRouteMatching, { shouldRejectTarget } from './route-matching.js';
|
|
4
|
+
import applyRouteMatching, { shouldRejectEncoding, shouldRejectTarget } from './route-matching.js';
|
|
5
5
|
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
6
6
|
export default class Request {
|
|
7
7
|
static stateProp = '__stonyxState';
|
|
@@ -97,6 +97,46 @@ export default class Request {
|
|
|
97
97
|
// half of it was covered. Do not re-merge them.
|
|
98
98
|
if (shouldRejectTarget(req))
|
|
99
99
|
return next('router');
|
|
100
|
+
// abofs/stonyx-rest-server#56 -- reject a request whose RAW target
|
|
101
|
+
// spells an UNRESERVED character (RFC 3986 2.3) as a percent-triplet,
|
|
102
|
+
// closing the authorization bypass against a hook that compares
|
|
103
|
+
// `req.path` OR `req.originalUrl` on any route class with a `:param`
|
|
104
|
+
// segment. Express decodes only `req.params`, so both raw fields see
|
|
105
|
+
// `%73ecret` while the handler is given `secret`.
|
|
106
|
+
//
|
|
107
|
+
// A SEPARATE line and a SEPARATE predicate from #54's above, and it
|
|
108
|
+
// must stay that way. Extending shouldRejectTarget()'s comparison
|
|
109
|
+
// cannot reach this: `target === canonical` for EVERY one of these
|
|
110
|
+
// spellings, because both sides carry the same encoded string.
|
|
111
|
+
// Reading #54's key here is worse than useless -- measured, gating
|
|
112
|
+
// this rule on `canonicalRoutes` returns `GET /enc/%73ecret` to 200
|
|
113
|
+
// under `REST_CANONICAL_ROUTES=false`, which is exactly the flag an
|
|
114
|
+
// absolute-form-proxy consumer must set. Killed by
|
|
115
|
+
// `test/unit/request-test.ts` AC5.
|
|
116
|
+
//
|
|
117
|
+
// The same three load-bearing properties as the line above apply
|
|
118
|
+
// verbatim -- outside `if (this.auth)`, BEFORE it, and rejecting with
|
|
119
|
+
// `next('router')` rather than a sendStatus that answers `text/plain`
|
|
120
|
+
// and becomes an oracle. They are killed here by
|
|
121
|
+
// `test/integration/rest-server-test.ts` #56 AC1.4 (the shape
|
|
122
|
+
// deep-equal against a genuine miss) and AC1.6
|
|
123
|
+
// (`/private/restricte%64` -> 404 rather than the hook's own 403),
|
|
124
|
+
// on a class WITH a hook.
|
|
125
|
+
//
|
|
126
|
+
// AC1.6, NOT AC1.5, and the difference is measured rather than
|
|
127
|
+
// reasoned. This comment named AC1.5 (`/private/%66ailure`) until
|
|
128
|
+
// #58's fix round; that assertion CANNOT fail for the relocation.
|
|
129
|
+
// For that request `private.ts`'s hook sees `req.path === '/%66ailure'`
|
|
130
|
+
// (no 505) and `req.params.id === 'failure'` (no 403), so it returns
|
|
131
|
+
// undefined and the relocated check still fires. Only
|
|
132
|
+
// `/private/restricte%64` trips a hook clause -- `req.params.id` is
|
|
133
|
+
// DECODED by express, so it equals `restricted` and answers 403 --
|
|
134
|
+
// which is what makes AC1.6 the assertion that reds. Measured: move
|
|
135
|
+
// this line below the `if (this.auth)` block, rebuild, and the suite
|
|
136
|
+
// reports 40 pass / 1 fail, the single failure being #56's AC1 with
|
|
137
|
+
// both of assertion 6's messages red and assertion 5 GREEN.
|
|
138
|
+
if (shouldRejectEncoding(req))
|
|
139
|
+
return next('router');
|
|
100
140
|
// Run auth after route matching so request.params is populated
|
|
101
141
|
if (this.auth) {
|
|
102
142
|
const status = this.auth(req, getState(req));
|
package/dist/request.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,OAA8G,MAAM,SAAS,CAAC;AACrI,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,kBAAkB,EAAE,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
|
package/dist/route-matching.d.ts
CHANGED
|
@@ -77,13 +77,14 @@ import type { Express, Request as ExpressRequest } from 'express';
|
|
|
77
77
|
* guarded handler running unauthenticated. `GET /public/` now returns 404, and
|
|
78
78
|
* the integration AC asserts exactly that.
|
|
79
79
|
*
|
|
80
|
-
*
|
|
80
|
+
* All four guards in this file are `!== false` for the same reason: these flags
|
|
81
|
+
* default to the
|
|
81
82
|
* truthy direction, so a truthy check fails OPEN for a consumer whose shipped
|
|
82
|
-
* config predates the key.
|
|
83
|
+
* config predates the key. All are also asserted at the unit tier for BOTH
|
|
83
84
|
* failure shapes -- key present-and-`undefined` and key absent as an own
|
|
84
|
-
* property -- in `test/unit/request-test.ts` (#47's AC6, #50's AC3
|
|
85
|
-
* integration tier cannot see
|
|
86
|
-
* fail-open guard leaves every integration assertion green.
|
|
85
|
+
* property -- in `test/unit/request-test.ts` (#47's AC6, #50's AC3, #54's AC2,
|
|
86
|
+
* #56's AC6). The integration tier cannot see any of them: with the shipped
|
|
87
|
+
* default `true`, a fail-open guard leaves every integration assertion green.
|
|
87
88
|
*/
|
|
88
89
|
export default function applyRouteMatching(api: Express): void;
|
|
89
90
|
/**
|
|
@@ -126,7 +127,7 @@ export default function applyRouteMatching(api: Express): void;
|
|
|
126
127
|
* are two separate properties with two separate assertions (AC1.11 and AC1.6);
|
|
127
128
|
* see src/request.ts.
|
|
128
129
|
*
|
|
129
|
-
* Guard polarity is `!== false`, matching
|
|
130
|
+
* Guard polarity is `!== false`, matching its three siblings, for the same measured
|
|
130
131
|
* reason: the secure value is the TRUTHY one, so a plain truthy check fails
|
|
131
132
|
* OPEN for any consumer whose shipped `restServer` block predates the key --
|
|
132
133
|
* the state every existing consumer is in, and reachable in practice because
|
|
@@ -142,4 +143,115 @@ export default function applyRouteMatching(api: Express): void;
|
|
|
142
143
|
* key present-and-`undefined` and key absent as an own property.
|
|
143
144
|
*/
|
|
144
145
|
export declare function shouldRejectTarget(req: ExpressRequest): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Decides whether a request must be rejected because its RAW request target
|
|
148
|
+
* spells an unreserved character as a percent-triplet
|
|
149
|
+
* (abofs/stonyx-rest-server#56).
|
|
150
|
+
*
|
|
151
|
+
* Closes a live authorization bypass on any route class carrying a `:param`
|
|
152
|
+
* segment. Express decodes `req.params` and NOTHING else -- `req.path` and
|
|
153
|
+
* `req.originalUrl` both stay percent-encoded -- so a consumer hook comparing
|
|
154
|
+
* either of those raw fields was walked past by re-spelling the id:
|
|
155
|
+
*
|
|
156
|
+
* GET /enc/secret -> 401 (hook fires)
|
|
157
|
+
* GET /enc/%73ecret -> 200 guarded handler, unauthenticated, id "secret"
|
|
158
|
+
*
|
|
159
|
+
* Both hook shapes are affected and neither is safer than the other; there is
|
|
160
|
+
* no spelling that defeats one and not the same-id comparison in the other.
|
|
161
|
+
* A third shape is worse still: a LITERAL guarded route co-registered with a
|
|
162
|
+
* sibling `/:id` (this repo's own `test/sample/requests/private.ts`) has the
|
|
163
|
+
* encoded spelling miss the literal layer and be ABSORBED by the param route,
|
|
164
|
+
* so the guard is walked past without the guarded handler ever running --
|
|
165
|
+
* measured `GET /private/failure` -> 505 vs `GET /private/%66ailure` -> 200.
|
|
166
|
+
*
|
|
167
|
+
* THE RULE IS AN UNRESERVED-OCTET SCAN, NOT A DECODE-AND-COMPARE. Two wrong
|
|
168
|
+
* implementations were built and measured, and each breaks a legitimate
|
|
169
|
+
* request:
|
|
170
|
+
*
|
|
171
|
+
* 1. `decodeURIComponent(target) !== target` -- rejects `/enc/sec%2fret`
|
|
172
|
+
* (404), which names the DISTINCT id `sec/ret`. The router SPLITS then
|
|
173
|
+
* DECODES; a whole-target decode decodes then splits, and the two
|
|
174
|
+
* disagree about `%2f` by construction. Killed by AC3.
|
|
175
|
+
* 2. decode until stable -- rejects `/enc/%2573ecret` (404), which names the
|
|
176
|
+
* legitimate id `%73ecret`. Express decodes EXACTLY ONCE, so `%2561` is
|
|
177
|
+
* not a bypass and a loop invents a false deny. Killed by AC4.
|
|
178
|
+
*
|
|
179
|
+
* WHY THIS IS NOT PART OF `shouldRejectTarget()` (#54), and why extending that
|
|
180
|
+
* comparison cannot work: for `GET /enc/%73ecret`, `originalUrl` is
|
|
181
|
+
* `/enc/%73ecret`, `baseUrl` is `/enc` and `path` is `/%73ecret`, so
|
|
182
|
+
* `target === canonical` -- both sides carry the SAME encoded string. The
|
|
183
|
+
* comparison is structurally blind to this axis and no change to it can see it.
|
|
184
|
+
*
|
|
185
|
+
* WHY IT IS A FOURTH KEY AND NOT A REUSE OF `canonicalRoutes`. Measured with
|
|
186
|
+
* the rule implemented correctly but gated on #54's key:
|
|
187
|
+
* `REST_CANONICAL_ROUTES=false` returns `GET /enc/%73ecret` to 200. That flag
|
|
188
|
+
* is exactly what a consumer behind an absolute-form-emitting forward proxy
|
|
189
|
+
* must set, so folding the two would hand precisely those consumers the
|
|
190
|
+
* encoding bypass as the price of staying up. Same argument the block above
|
|
191
|
+
* makes for why #50 is not a rename of #47. Pinned by
|
|
192
|
+
* `test/unit/request-test.ts` AC5, which also asserts -- in that same state --
|
|
193
|
+
* that #54's own vector IS re-opened, so an implementation that simply ignores
|
|
194
|
+
* `canonicalRoutes` cannot pass it vacuously.
|
|
195
|
+
*
|
|
196
|
+
* TIMING CONTRACT: identical to `shouldRejectTarget()` and NOT to the two
|
|
197
|
+
* settings above. Read per request, inside the handler closure in
|
|
198
|
+
* `Request.registerCalls()`; there is no lazy-materialisation hazard, so do not
|
|
199
|
+
* move it into `applyRouteMatching()`. The caller must reject with
|
|
200
|
+
* `next('router')`, and must run this BEFORE `this.auth` as well as outside
|
|
201
|
+
* `if (this.auth)` -- see src/request.ts.
|
|
202
|
+
*
|
|
203
|
+
* Guard polarity is `!== false`, matching all three siblings, for the same
|
|
204
|
+
* measured reason: the secure value is the TRUTHY one, so `=== true` fails OPEN
|
|
205
|
+
* for any consumer whose shipped `restServer` block predates the key. The
|
|
206
|
+
* integration tier CANNOT see that mutation -- with the shipped default `true`
|
|
207
|
+
* every integration assertion stays green -- so it is `test/unit/request-test.ts`
|
|
208
|
+
* AC6 that kills it, probing the key present-and-`undefined` and absent as an
|
|
209
|
+
* own property separately.
|
|
210
|
+
*
|
|
211
|
+
* WHAT THIS DOES NOT CLOSE, stated here rather than left implied. It cannot
|
|
212
|
+
* give each decoded id exactly one accepted spelling, because everything the
|
|
213
|
+
* allowlist above does NOT cover must remain encodable: `/enc/a+b` and
|
|
214
|
+
* `/enc/a%2Bb` both name the id `a+b`, and `/enc/sec%2fret` and
|
|
215
|
+
* `/enc/sec%2Fret` both name `sec/ret`.
|
|
216
|
+
*
|
|
217
|
+
* THE RESIDUAL IS WIDER THAN "RESERVED CHARACTERS" AND MUST NOT BE WRITTEN
|
|
218
|
+
* DOWN THAT WAY. An octet outside `[A-Za-z0-9-._~]` keeps more than one
|
|
219
|
+
* accepted spelling when its hex carries a letter digit (upper- and lower-case
|
|
220
|
+
* hex) or when a client may also send it literally. That is every reserved
|
|
221
|
+
* character, every non-ASCII byte AND every control octet whose hex carries a
|
|
222
|
+
* letter digit. Measured through a real listener against this predicate, on a
|
|
223
|
+
* deny list holding NO reserved character at all:
|
|
224
|
+
*
|
|
225
|
+
* GET /i18n/caf%C3%A9 -> 401 GET /i18n/caf%c3%a9 -> 200, id "café"
|
|
226
|
+
* GET /i18n/%E5%8C%97%E4%BA%AC -> 401 GET /i18n/%e5%8c%97%e4%ba%ac -> 200, id "北京"
|
|
227
|
+
* GET /i18n/a%0Db -> 401 GET /i18n/a%0db -> 200, id "a\rb"
|
|
228
|
+
*
|
|
229
|
+
* A consumer whose ids are i18n text reads "any id containing a reserved
|
|
230
|
+
* character" as not applying to them. It does. The three docs that carried the
|
|
231
|
+
* narrow wording (`README.md`, `docs/project-structure.md`,
|
|
232
|
+
* `docs/agents/security-reviewer.md`) were widened to this scope rather than
|
|
233
|
+
* this comment being narrowed to theirs.
|
|
234
|
+
*
|
|
235
|
+
* NOT the whole complement of the unreserved set, and this qualifier is load-
|
|
236
|
+
* bearing rather than pedantry -- the sentence above is stated as measured, so
|
|
237
|
+
* it must not over-warn either. Measured counterexamples: `%21` and `%40` are
|
|
238
|
+
* reserved and carry no letter hex digit, so they alias literal-versus-encoded
|
|
239
|
+
* rather than by hex case; `%00` and `%09` have exactly one accepted spelling
|
|
240
|
+
* and do not alias at all; `%90` is a 400 (invalid UTF-8), not an alias.
|
|
241
|
+
*
|
|
242
|
+
* So a hook comparing a raw path string REMAINS UNSOUND for any id carrying an
|
|
243
|
+
* octet outside `[A-Za-z0-9-._~]` that keeps more than one accepted spelling,
|
|
244
|
+
* and `req.params` -- which express decodes, and which is populated before
|
|
245
|
+
* `auth()` runs -- is the sound idiom. That
|
|
246
|
+
* residual is the consumer's comparison to own; the module cannot close it
|
|
247
|
+
* without 404ing encodings clients are required to emit.
|
|
248
|
+
*
|
|
249
|
+
* SCOPE LIMIT, separate from the residual above: this predicate is called from
|
|
250
|
+
* `Request.registerCalls()`, so it covers the routes mounted from request
|
|
251
|
+
* classes and nothing else. A route registered directly on the public
|
|
252
|
+
* `RestServer.instance.api` gets none of it -- measured,
|
|
253
|
+
* `GET /direct/%73ecret` -> 200 with `id "secret"` while `GET /enc/%73ecret`
|
|
254
|
+
* -> 404. Same registration-site limit `canonicalRoutes` (#54) has.
|
|
255
|
+
*/
|
|
256
|
+
export declare function shouldRejectEncoding(req: ExpressRequest): boolean;
|
|
145
257
|
//# sourceMappingURL=route-matching.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,SAAS,CAAC;AAGlE
|
|
1
|
+
{"version":3,"file":"route-matching.d.ts","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,SAAS,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAuB/D;AA2BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAkBjE"}
|
package/dist/route-matching.js
CHANGED
|
@@ -77,13 +77,14 @@ import config from 'stonyx/config';
|
|
|
77
77
|
* guarded handler running unauthenticated. `GET /public/` now returns 404, and
|
|
78
78
|
* the integration AC asserts exactly that.
|
|
79
79
|
*
|
|
80
|
-
*
|
|
80
|
+
* All four guards in this file are `!== false` for the same reason: these flags
|
|
81
|
+
* default to the
|
|
81
82
|
* truthy direction, so a truthy check fails OPEN for a consumer whose shipped
|
|
82
|
-
* config predates the key.
|
|
83
|
+
* config predates the key. All are also asserted at the unit tier for BOTH
|
|
83
84
|
* failure shapes -- key present-and-`undefined` and key absent as an own
|
|
84
|
-
* property -- in `test/unit/request-test.ts` (#47's AC6, #50's AC3
|
|
85
|
-
* integration tier cannot see
|
|
86
|
-
* fail-open guard leaves every integration assertion green.
|
|
85
|
+
* property -- in `test/unit/request-test.ts` (#47's AC6, #50's AC3, #54's AC2,
|
|
86
|
+
* #56's AC6). The integration tier cannot see any of them: with the shipped
|
|
87
|
+
* default `true`, a fail-open guard leaves every integration assertion green.
|
|
87
88
|
*/
|
|
88
89
|
export default function applyRouteMatching(api) {
|
|
89
90
|
if (config.restServer?.caseSensitiveRoutes !== false)
|
|
@@ -131,7 +132,7 @@ export default function applyRouteMatching(api) {
|
|
|
131
132
|
* are two separate properties with two separate assertions (AC1.11 and AC1.6);
|
|
132
133
|
* see src/request.ts.
|
|
133
134
|
*
|
|
134
|
-
* Guard polarity is `!== false`, matching
|
|
135
|
+
* Guard polarity is `!== false`, matching its three siblings, for the same measured
|
|
135
136
|
* reason: the secure value is the TRUTHY one, so a plain truthy check fails
|
|
136
137
|
* OPEN for any consumer whose shipped `restServer` block predates the key --
|
|
137
138
|
* the state every existing consumer is in, and reachable in practice because
|
|
@@ -168,4 +169,155 @@ export function shouldRejectTarget(req) {
|
|
|
168
169
|
const canonical = req.path === '/' && req.baseUrl ? req.baseUrl : req.baseUrl + req.path;
|
|
169
170
|
return target !== canonical;
|
|
170
171
|
}
|
|
172
|
+
// RFC 3986 §2.3 UNRESERVED = ALPHA / DIGIT / "-" / "." / "_" / "~".
|
|
173
|
+
//
|
|
174
|
+
// These are the characters a URI generator MUST NOT percent-encode and that a
|
|
175
|
+
// normaliser MUST decode (§6.2.2.2), so an encoded one carries no information
|
|
176
|
+
// a client is ever required to send. Everything else -- every RESERVED
|
|
177
|
+
// character and every non-ASCII octet -- stays encodable, which is the whole
|
|
178
|
+
// reason this is an allowlist of octets rather than a ban on triplets. See
|
|
179
|
+
// `shouldRejectEncoding()` below.
|
|
180
|
+
const UNRESERVED_OCTET = /^[A-Za-z0-9\-._~]$/;
|
|
181
|
+
// A percent-triplet: `%` followed by exactly two hex digits, either case.
|
|
182
|
+
//
|
|
183
|
+
// A `%` can never be part of ANOTHER triplet's hex digits, because `%` is not a
|
|
184
|
+
// hex digit -- so scanning left to right without skipping cannot produce an
|
|
185
|
+
// overlapping false match. `%2561` therefore yields exactly one candidate
|
|
186
|
+
// (`%25`), which is the property AC4 pins.
|
|
187
|
+
//
|
|
188
|
+
// Malformed and over-long escapes (`%zz`, `%`, `%6`, `%c1%a1`, `%e0%81%a1`) are
|
|
189
|
+
// deliberately NOT this function's business: `router@2.2.0`'s `decodeParam`
|
|
190
|
+
// (lib/layer.js:225) answers 400 for them before any handler or hook runs.
|
|
191
|
+
// Verified here rather than imported -- measured 400 both before and after this
|
|
192
|
+
// change. None of those octets is unreserved, and the first three are not valid
|
|
193
|
+
// triplets at all, so the rule does not touch them either way.
|
|
194
|
+
const PERCENT_TRIPLET = /%([0-9A-Fa-f]{2})/g;
|
|
195
|
+
/**
|
|
196
|
+
* Decides whether a request must be rejected because its RAW request target
|
|
197
|
+
* spells an unreserved character as a percent-triplet
|
|
198
|
+
* (abofs/stonyx-rest-server#56).
|
|
199
|
+
*
|
|
200
|
+
* Closes a live authorization bypass on any route class carrying a `:param`
|
|
201
|
+
* segment. Express decodes `req.params` and NOTHING else -- `req.path` and
|
|
202
|
+
* `req.originalUrl` both stay percent-encoded -- so a consumer hook comparing
|
|
203
|
+
* either of those raw fields was walked past by re-spelling the id:
|
|
204
|
+
*
|
|
205
|
+
* GET /enc/secret -> 401 (hook fires)
|
|
206
|
+
* GET /enc/%73ecret -> 200 guarded handler, unauthenticated, id "secret"
|
|
207
|
+
*
|
|
208
|
+
* Both hook shapes are affected and neither is safer than the other; there is
|
|
209
|
+
* no spelling that defeats one and not the same-id comparison in the other.
|
|
210
|
+
* A third shape is worse still: a LITERAL guarded route co-registered with a
|
|
211
|
+
* sibling `/:id` (this repo's own `test/sample/requests/private.ts`) has the
|
|
212
|
+
* encoded spelling miss the literal layer and be ABSORBED by the param route,
|
|
213
|
+
* so the guard is walked past without the guarded handler ever running --
|
|
214
|
+
* measured `GET /private/failure` -> 505 vs `GET /private/%66ailure` -> 200.
|
|
215
|
+
*
|
|
216
|
+
* THE RULE IS AN UNRESERVED-OCTET SCAN, NOT A DECODE-AND-COMPARE. Two wrong
|
|
217
|
+
* implementations were built and measured, and each breaks a legitimate
|
|
218
|
+
* request:
|
|
219
|
+
*
|
|
220
|
+
* 1. `decodeURIComponent(target) !== target` -- rejects `/enc/sec%2fret`
|
|
221
|
+
* (404), which names the DISTINCT id `sec/ret`. The router SPLITS then
|
|
222
|
+
* DECODES; a whole-target decode decodes then splits, and the two
|
|
223
|
+
* disagree about `%2f` by construction. Killed by AC3.
|
|
224
|
+
* 2. decode until stable -- rejects `/enc/%2573ecret` (404), which names the
|
|
225
|
+
* legitimate id `%73ecret`. Express decodes EXACTLY ONCE, so `%2561` is
|
|
226
|
+
* not a bypass and a loop invents a false deny. Killed by AC4.
|
|
227
|
+
*
|
|
228
|
+
* WHY THIS IS NOT PART OF `shouldRejectTarget()` (#54), and why extending that
|
|
229
|
+
* comparison cannot work: for `GET /enc/%73ecret`, `originalUrl` is
|
|
230
|
+
* `/enc/%73ecret`, `baseUrl` is `/enc` and `path` is `/%73ecret`, so
|
|
231
|
+
* `target === canonical` -- both sides carry the SAME encoded string. The
|
|
232
|
+
* comparison is structurally blind to this axis and no change to it can see it.
|
|
233
|
+
*
|
|
234
|
+
* WHY IT IS A FOURTH KEY AND NOT A REUSE OF `canonicalRoutes`. Measured with
|
|
235
|
+
* the rule implemented correctly but gated on #54's key:
|
|
236
|
+
* `REST_CANONICAL_ROUTES=false` returns `GET /enc/%73ecret` to 200. That flag
|
|
237
|
+
* is exactly what a consumer behind an absolute-form-emitting forward proxy
|
|
238
|
+
* must set, so folding the two would hand precisely those consumers the
|
|
239
|
+
* encoding bypass as the price of staying up. Same argument the block above
|
|
240
|
+
* makes for why #50 is not a rename of #47. Pinned by
|
|
241
|
+
* `test/unit/request-test.ts` AC5, which also asserts -- in that same state --
|
|
242
|
+
* that #54's own vector IS re-opened, so an implementation that simply ignores
|
|
243
|
+
* `canonicalRoutes` cannot pass it vacuously.
|
|
244
|
+
*
|
|
245
|
+
* TIMING CONTRACT: identical to `shouldRejectTarget()` and NOT to the two
|
|
246
|
+
* settings above. Read per request, inside the handler closure in
|
|
247
|
+
* `Request.registerCalls()`; there is no lazy-materialisation hazard, so do not
|
|
248
|
+
* move it into `applyRouteMatching()`. The caller must reject with
|
|
249
|
+
* `next('router')`, and must run this BEFORE `this.auth` as well as outside
|
|
250
|
+
* `if (this.auth)` -- see src/request.ts.
|
|
251
|
+
*
|
|
252
|
+
* Guard polarity is `!== false`, matching all three siblings, for the same
|
|
253
|
+
* measured reason: the secure value is the TRUTHY one, so `=== true` fails OPEN
|
|
254
|
+
* for any consumer whose shipped `restServer` block predates the key. The
|
|
255
|
+
* integration tier CANNOT see that mutation -- with the shipped default `true`
|
|
256
|
+
* every integration assertion stays green -- so it is `test/unit/request-test.ts`
|
|
257
|
+
* AC6 that kills it, probing the key present-and-`undefined` and absent as an
|
|
258
|
+
* own property separately.
|
|
259
|
+
*
|
|
260
|
+
* WHAT THIS DOES NOT CLOSE, stated here rather than left implied. It cannot
|
|
261
|
+
* give each decoded id exactly one accepted spelling, because everything the
|
|
262
|
+
* allowlist above does NOT cover must remain encodable: `/enc/a+b` and
|
|
263
|
+
* `/enc/a%2Bb` both name the id `a+b`, and `/enc/sec%2fret` and
|
|
264
|
+
* `/enc/sec%2Fret` both name `sec/ret`.
|
|
265
|
+
*
|
|
266
|
+
* THE RESIDUAL IS WIDER THAN "RESERVED CHARACTERS" AND MUST NOT BE WRITTEN
|
|
267
|
+
* DOWN THAT WAY. An octet outside `[A-Za-z0-9-._~]` keeps more than one
|
|
268
|
+
* accepted spelling when its hex carries a letter digit (upper- and lower-case
|
|
269
|
+
* hex) or when a client may also send it literally. That is every reserved
|
|
270
|
+
* character, every non-ASCII byte AND every control octet whose hex carries a
|
|
271
|
+
* letter digit. Measured through a real listener against this predicate, on a
|
|
272
|
+
* deny list holding NO reserved character at all:
|
|
273
|
+
*
|
|
274
|
+
* GET /i18n/caf%C3%A9 -> 401 GET /i18n/caf%c3%a9 -> 200, id "café"
|
|
275
|
+
* GET /i18n/%E5%8C%97%E4%BA%AC -> 401 GET /i18n/%e5%8c%97%e4%ba%ac -> 200, id "北京"
|
|
276
|
+
* GET /i18n/a%0Db -> 401 GET /i18n/a%0db -> 200, id "a\rb"
|
|
277
|
+
*
|
|
278
|
+
* A consumer whose ids are i18n text reads "any id containing a reserved
|
|
279
|
+
* character" as not applying to them. It does. The three docs that carried the
|
|
280
|
+
* narrow wording (`README.md`, `docs/project-structure.md`,
|
|
281
|
+
* `docs/agents/security-reviewer.md`) were widened to this scope rather than
|
|
282
|
+
* this comment being narrowed to theirs.
|
|
283
|
+
*
|
|
284
|
+
* NOT the whole complement of the unreserved set, and this qualifier is load-
|
|
285
|
+
* bearing rather than pedantry -- the sentence above is stated as measured, so
|
|
286
|
+
* it must not over-warn either. Measured counterexamples: `%21` and `%40` are
|
|
287
|
+
* reserved and carry no letter hex digit, so they alias literal-versus-encoded
|
|
288
|
+
* rather than by hex case; `%00` and `%09` have exactly one accepted spelling
|
|
289
|
+
* and do not alias at all; `%90` is a 400 (invalid UTF-8), not an alias.
|
|
290
|
+
*
|
|
291
|
+
* So a hook comparing a raw path string REMAINS UNSOUND for any id carrying an
|
|
292
|
+
* octet outside `[A-Za-z0-9-._~]` that keeps more than one accepted spelling,
|
|
293
|
+
* and `req.params` -- which express decodes, and which is populated before
|
|
294
|
+
* `auth()` runs -- is the sound idiom. That
|
|
295
|
+
* residual is the consumer's comparison to own; the module cannot close it
|
|
296
|
+
* without 404ing encodings clients are required to emit.
|
|
297
|
+
*
|
|
298
|
+
* SCOPE LIMIT, separate from the residual above: this predicate is called from
|
|
299
|
+
* `Request.registerCalls()`, so it covers the routes mounted from request
|
|
300
|
+
* classes and nothing else. A route registered directly on the public
|
|
301
|
+
* `RestServer.instance.api` gets none of it -- measured,
|
|
302
|
+
* `GET /direct/%73ecret` -> 200 with `id "secret"` while `GET /enc/%73ecret`
|
|
303
|
+
* -> 404. Same registration-site limit `canonicalRoutes` (#54) has.
|
|
304
|
+
*/
|
|
305
|
+
export function shouldRejectEncoding(req) {
|
|
306
|
+
// `!== false`, not `=== false` and not a truthy check: the polarity is the
|
|
307
|
+
// load-bearing part and it should read identically to the three guards above.
|
|
308
|
+
const enforced = config.restServer?.canonicalEncoding !== false;
|
|
309
|
+
if (!enforced)
|
|
310
|
+
return false;
|
|
311
|
+
// Raw, unparsed, and only the query string removed -- by string split, for
|
|
312
|
+
// the same reason #54 gives: parsing would launder the exact string the
|
|
313
|
+
// consumer's hook is exposed to. The query is stripped because a query string
|
|
314
|
+
// is a legitimately variable part of a request target and may carry any
|
|
315
|
+
// encoding at all; `?name=%61` is a normal request and must not 404.
|
|
316
|
+
const target = req.originalUrl.split('?')[0];
|
|
317
|
+
for (const [, hex] of target.matchAll(PERCENT_TRIPLET)) {
|
|
318
|
+
if (UNRESERVED_OCTET.test(String.fromCharCode(parseInt(hex, 16))))
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
171
323
|
//# sourceMappingURL=route-matching.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC
|
|
1
|
+
{"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAY;IACrD,IAAI,MAAM,CAAC,UAAU,EAAE,mBAAmB,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;IAC9F,IAAI,MAAM,CAAC,UAAU,EAAE,YAAY,KAAK,KAAK;QAAE,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAmB;IACpD,6EAA6E;IAC7E,4EAA4E;IAC5E,8BAA8B;IAC9B,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,EAAE,eAAe,KAAK,KAAK,CAAC;IAC9D,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,oEAAoE;IACpE,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7C,yEAAyE;IACzE,4EAA4E;IAC5E,EAAE;IACF,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,6EAA6E;IAC7E,iEAAiE;IACjE,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;IAEzF,OAAO,MAAM,KAAK,SAAS,CAAC;AAC9B,CAAC;AAED,oEAAoE;AACpE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,uEAAuE;AACvE,6EAA6E;AAC7E,2EAA2E;AAC3E,kCAAkC;AAClC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AAE9C,0EAA0E;AAC1E,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,0EAA0E;AAC1E,2CAA2C;AAC3C,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,2EAA2E;AAC3E,gFAAgF;AAChF,gFAAgF;AAChF,+DAA+D;AAC/D,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAmB;IACtD,2EAA2E;IAC3E,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,EAAE,iBAAiB,KAAK,KAAK,CAAC;IAChE,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,2EAA2E;IAC3E,wEAAwE;IACxE,8EAA8E;IAC9E,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7C,KAAK,MAAM,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACvD,IAAI,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IAClF,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|