@stonyx/rest-server 0.2.1-alpha.22 → 0.2.1-alpha.24

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 CHANGED
@@ -81,23 +81,30 @@ Configuration is read from `stonyx/config` under `restServer`:
81
81
  | `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
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
+ | `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. |
84
85
  | `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. |
85
86
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
86
87
 
87
88
  ### Route Matching Strictness
88
89
 
89
- Routes match **case-sensitively and strictly by default**. Two settings, both
90
- on, both applied at both express construction sites:
90
+ Routes match **case-sensitively, strictly, and only at their canonical target
91
+ by default**. Three controls, all on:
91
92
 
92
- | axis | setting | example that no longer matches |
93
+ | axis | control | example that no longer matches |
93
94
  |---|---|---|
94
- | casing | `case sensitive routing` | `GET /users/Success` -> does not reach `/success` |
95
- | trailing slash | `strict routing` | `GET /users/success/` -> does not reach `/success` |
95
+ | casing | `case sensitive routing` (setting) | `GET /users/Success` -> does not reach `/success` |
96
+ | trailing slash | `strict routing` (setting) | `GET /users/success/` -> does not reach `/success` |
97
+ | canonical target | `canonicalRoutes` (per-request check) | `GET /users/` and `GET http://host/users` -> do not reach the mounted `/users` class |
98
+
99
+ The first two are express settings applied at both construction sites. The
100
+ third is **not a setting** — no express setting can express it — it is a
101
+ per-request comparison of the raw request target against the path express
102
+ matched, run ahead of your `auth` hook. See
103
+ [`src/route-matching.ts`](src/route-matching.ts).
96
104
 
97
105
  Read [What this does not do](#what-this-does-not-do) and
98
- [Upgrading](#upgrading-behaviour-changes) before you rely on that. Two things
99
- the table does not say: "does not reach the handler" is not the same as "404",
100
- and one edge of the trailing-slash axis is **not** closed and cannot be.
106
+ [Upgrading](#upgrading-behaviour-changes) before you rely on that. One thing the
107
+ table does not say: "does not reach the handler" is not the same as "404".
101
108
 
102
109
  This is deliberate and security-relevant. Express matches both case-insensitively
103
110
  and slash-insensitively by default, which means any authorization written
@@ -128,11 +135,10 @@ be the exact registered spelling, in the exact registered casing, with no
128
135
  trailing slash. That closes [#47](https://github.com/abofs/stonyx-rest-server/issues/47)
129
136
  and [#50](https://github.com/abofs/stonyx-rest-server/issues/50).
130
137
 
131
- #### What this does not do
138
+ #### The canonical-target check (`canonicalRoutes`)
132
139
 
133
- **It does not close the trailing slash on a mount root, and no setting can.**
134
- This is the one edge that remains open, so do not read the section above as
135
- closing the class outright:
140
+ **No express *setting* closes the trailing slash on a mount root**, and that has
141
+ not changed:
136
142
 
137
143
  ```
138
144
  GET /public -> req.path '/' req.originalUrl '/public'
@@ -140,13 +146,47 @@ GET /public/ -> req.path '/' req.originalUrl '/public/'
140
146
  ```
141
147
 
142
148
  Express's router applies mount-prefix matching with `strict: false`
143
- unconditionally (`router@2.2.0`, `index.js:400-401`), so both forms reach the
144
- mounted route class and both arrive with `req.path === '/'`. A hook authorizing
145
- on `req.path` cannot tell them apart, so for that hook there is no asymmetry to
146
- exploit. **A hook comparing `req.originalUrl` still sees two different strings,
147
- and `strictRoutes` does not change that.** If your authorization compares
148
- `req.originalUrl` rather than `req.path`, keep whatever URL normalization you
149
- have.
149
+ unconditionally (`router@2.2.0`; the file-and-line citation is in
150
+ [`docs/project-structure.md`](docs/project-structure.md) § *Strict routing
151
+ (#50)*), so both forms reach the mounted route class and both arrive with
152
+ `req.path === '/'`. A hook authorizing on `req.path` cannot tell them apart, so
153
+ for that hook there is no asymmetry to exploit — and there is nothing for
154
+ `strict routing` to reject. **A hook comparing `req.originalUrl` sees two
155
+ different strings**, and that was a live authorization bypass.
156
+
157
+ `canonicalRoutes` closes it, as a per-request check rather than a setting
158
+ ([#54](https://github.com/abofs/stonyx-rest-server/issues/54)). Before your
159
+ `auth` hook runs, the raw request target is compared against the path express
160
+ matched, and a mismatch is rejected as a plain 404. It closes **two** vectors
161
+ against `req.originalUrl` — the field express does not normalize:
162
+
163
+ ```
164
+ before after
165
+ GET /admin 401 401 (hook fires, request blocked)
166
+ GET /admin/ 200 404 (hook never fired; now a miss)
167
+ GET http://host/admin 200 404 (absolute-form; hook never fired)
168
+ GET http://host/admin/settings 200 404 (absolute-form, and every other route)
169
+ ```
170
+
171
+ The second vector is the one to check first. [RFC 9112
172
+ §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2) permits an
173
+ **absolute-form** request target, express routes it, and it hands your hook the
174
+ whole URI — `req.originalUrl === "http://host/admin"`. Unlike the mount-root
175
+ slash, that affects **every route**, not one edge.
176
+
177
+ The target is compared **raw**. It is not parsed, normalized or resolved first:
178
+ normalizing it would launder exactly the string your hook is exposed to and
179
+ re-open the absolute-form vector by construction. Only the query string is
180
+ removed before comparison, so `GET /admin?x=1` still reaches your hook — **strip
181
+ the query yourself** if your hook compares `req.originalUrl` against a fixed
182
+ path, or it will not match. Rejections are indistinguishable from a genuine miss
183
+ by design; see [Upgrading](#upgrading-behaviour-changes).
184
+
185
+ Routes registered *with* a literal trailing slash are unaffected — their
186
+ canonical target carries the slash. So are param routes, index-mounted route
187
+ classes, and query strings on canonical paths.
188
+
189
+ #### What this does not do
150
190
 
151
191
  **It does not normalize path *parameter values*.** If your `auth()` hook rejects
152
192
  `params.id === 'restricted'`, then `GET /private/RESTRICTED` still reaches the
@@ -175,7 +215,29 @@ another variant of the bug above.
175
215
 
176
216
  #### Upgrading: behaviour changes
177
217
 
178
- Both settings change which requests match, so both are consumer-visible.
218
+ All three controls change which requests match, so all three are
219
+ consumer-visible.
220
+
221
+ **Clients or forward proxies sending absolute-form request targets now get 404
222
+ on every route.** `GET http://host/admin HTTP/1.1` is a legal request target
223
+ ([RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2)) and
224
+ express used to route it. It is now rejected, on **every** route — for a client
225
+ that emits it, this is a total outage, not a partial one, and it is the largest
226
+ blast radius in this change. Reverse proxies in normal use (nginx, HAProxy,
227
+ AWS ALB) send origin-form and are unaffected; **forward** proxies and
228
+ hand-rolled HTTP clients are the exposure. Remediation is
229
+ `REST_CANONICAL_ROUTES=false`, or fix the client.
230
+
231
+ **`GET /route/` at a mounted route class's root now returns 404.** Previously
232
+ 200. If a client appends a trailing slash to a mount root, it stops working.
233
+
234
+ Both rejections are **indistinguishable from a route that was never
235
+ registered** — same status, same `Content-Type`, same headers — which is the
236
+ intended security property: a distinguishable rejection is an oracle telling an
237
+ attacker the route exists and was merely spelled wrong. Combined with this
238
+ module emitting **no request logging**, a broken client shows up as a bare
239
+ `Cannot GET …` with nothing at all on the server side. **Check this first if
240
+ routes start 404ing after upgrade.**
179
241
 
180
242
  **`GET /health/` now returns 404.** `GET /health` is unaffected. This is the
181
243
  change most likely to page someone, and it is an **availability** problem rather
@@ -201,21 +263,31 @@ no log line and no stack, so it looks like a deploy that dropped a route.
201
263
 
202
264
  #### Opting out
203
265
 
204
- Two separate flags, one per axis:
266
+ Three separate flags, one per axis:
205
267
 
206
268
  ```bash
207
269
  REST_CASE_SENSITIVE_ROUTES=false # restores case-insensitive matching (#47)
208
270
  REST_STRICT_ROUTES=false # restores trailing-slash tolerance (#50)
271
+ REST_CANONICAL_ROUTES=false # restores non-canonical request targets (#54)
209
272
  ```
210
273
 
211
- or equivalently `restServer: { caseSensitiveRoutes: false, strictRoutes: false }`.
274
+ or equivalently
275
+ `restServer: { caseSensitiveRoutes: false, strictRoutes: false, canonicalRoutes: false }`.
212
276
 
213
- **They are deliberately separate keys, and neither implies the other.** Slash
277
+ **They are deliberately separate keys, and none implies the others.** Slash
214
278
  tolerance is a legitimate need — a health-check URL you cannot change today is
215
279
  the common case. Casing tolerance almost never is. Folding them into one flag
216
280
  would force anyone who needs the first to accept the second, which is why a
217
281
  consumer who took the `#47` opt-out still has to set `REST_STRICT_ROUTES=false`
218
- separately to keep trailing slashes working.
282
+ separately to keep trailing slashes working. `REST_CANONICAL_ROUTES=false` is
283
+ separate for the same reason: a consumer who needs mount-root slash tolerance
284
+ should not have to re-open `#50`'s sub-path bypass to get it.
285
+
286
+ `REST_CANONICAL_ROUTES=false` is a **temporary remediation**, not a
287
+ configuration to run on. It re-opens both `#54` vectors at once — the mount-root
288
+ slash *and* the absolute-form target — against any hook authorizing on
289
+ `req.originalUrl`. It is env-only, so restoring service does not need a
290
+ redeploy; use it to stop the bleeding, then fix the client and remove it.
219
291
 
220
292
  **Each flag restores the corresponding vulnerability described above** — the
221
293
  URL-based authorization in your application becomes bypassable along that axis
@@ -1,4 +1,5 @@
1
1
  const {
2
+ REST_CANONICAL_ROUTES,
2
3
  REST_CASE_SENSITIVE_ROUTES,
3
4
  REST_CORS_ORIGIN,
4
5
  REST_CORS_METHODS,
@@ -63,6 +64,57 @@ const config = {
63
64
  // live assertion on this default.
64
65
  strictRoutes: REST_STRICT_ROUTES !== 'false',
65
66
 
67
+ // Secure by default, same polarity and same reasoning as the two keys above:
68
+ // a request whose RAW target is not the canonical path express matched is
69
+ // rejected with a plain 404 before the consumer's `auth` hook runs
70
+ // (abofs/stonyx-rest-server#54). This is NOT an express setting -- it is a
71
+ // per-request check in src/route-matching.ts (`shouldRejectTarget`), called
72
+ // from the handler closure in src/request.ts.
73
+ //
74
+ // BEHAVIOUR CHANGE for consumers upgrading, on TWO axes:
75
+ // 1. `GET /route/` at a mounted route class's ROOT now returns 404.
76
+ // 2. Clients or forward proxies emitting an ABSOLUTE-FORM request target
77
+ // (`GET http://host/admin HTTP/1.1`, RFC 9112 3.2.2) now receive 404 on
78
+ // EVERY route. This is the larger blast radius: for such a client it is
79
+ // a total outage, not a partial one. Reverse proxies in normal use
80
+ // (nginx, HAProxy, ALB) send origin-form and are unaffected.
81
+ // This module emits no request log, so both look like a dropped route.
82
+ //
83
+ // Opt out with REST_CANONICAL_ROUTES=false only as a temporary remediation --
84
+ // it RE-OPENS the bypass. Separate key from REST_STRICT_ROUTES and
85
+ // REST_CASE_SENSITIVE_ROUTES on purpose: coupling it to strictness would
86
+ // force a consumer who needs mount-root slash tolerance to also re-open #50's
87
+ // sub-path bypass.
88
+ //
89
+ // Note trustProxy below deliberately uses `=== 'true'` instead. That is not
90
+ // an inconsistency to "fix": its safe default is FALSY, so a truthy check
91
+ // already fails closed for it. The rule is "the guard must fail toward the
92
+ // safe value", not "all guards look alike".
93
+ //
94
+ // DELIBERATELY NOT PINNED in test/config/environment.ts -- do not "fix" this
95
+ // as part of abofs/stonyx-rest-server#43. Same trap as the two keys above,
96
+ // and now measured for all three: pin `canonicalRoutes: true` in
97
+ // test/config/environment.ts AND invert this line to `=== 'true'`, and the
98
+ // suite reports 34 pass / 0 fail while an insecure default ships. The pin is
99
+ // quieter AND weaker than no pin, which is the outcome pinning was supposed
100
+ // to prevent.
101
+ //
102
+ // Unpinned, inverting this line alone reports 32 pass / 2 fail: #54's
103
+ // integration AC1 and #50's AC2. AC2 (unit) stays GREEN under that mutation,
104
+ // because it sets `canonicalRoutes` on the config object directly and so
105
+ // guards src/route-matching.ts's READ rather than this default -- the two
106
+ // assertions cover different halves and neither subsumes the other.
107
+ // Conversely, weakening the READ to `=== true` reports 33 pass / 1 fail with
108
+ // AC2 as the only failure and AC1 fully green. Both measurements are on the
109
+ // #54 branch head.
110
+ //
111
+ // The cost is that the suite is ambient-sensitive here
112
+ // (`REST_CANONICAL_ROUTES=false pnpm test` => 32 pass / 2 fail), but it fails
113
+ // LOUDLY, so there is no false green. Closing #43 for any of the three keys
114
+ // needs subprocess-based env isolation this repo does not have; any fix must
115
+ // keep a live assertion on this default.
116
+ canonicalRoutes: REST_CANONICAL_ROUTES !== 'false',
117
+
66
118
  enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
67
119
  origin: REST_CORS_ORIGIN ?? '*',
68
120
  methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAKlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAgBhB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAKlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAwBhB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
package/dist/main.js CHANGED
@@ -29,11 +29,19 @@ export default class RestServer {
29
29
  return RestServer.instance;
30
30
  RestServer.instance = this;
31
31
  this.api = express();
32
- // Closes the mount segment (/PUBLIC/...) for abofs/stonyx-rest-server#47.
32
+ // Applies BOTH route-matching settings: case sensitive routing
33
+ // (abofs/stonyx-rest-server#47) and strict routing (#50). The two do not
34
+ // have the same role at this site:
35
+ // - #47: this call closes the mount segment (/PUBLIC/...). The matching
36
+ // call in Request's constructor closes sub-paths; both are required.
37
+ // - #50: this call closes exactly /health/, the only route registered
38
+ // directly on this app. It has NO security role for #50 -- do not
39
+ // describe it as having one. Router.prototype.use hardcodes
40
+ // `strict: false`, so mount segments are strict-immune, and the call in
41
+ // Request's constructor closes the trailing-slash bypass on its own.
33
42
  // Must stay in the constructor: the router is materialized lazily on first
34
43
  // route registration, so applying this after setupRouter() is silently
35
- // ineffective. The matching call in Request's constructor is what closes
36
- // sub-paths -- see src/route-matching.ts for why both are required.
44
+ // ineffective. See src/route-matching.ts for the per-site split.
37
45
  applyRouteMatching(this.api);
38
46
  }
39
47
  static close() {
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
@@ -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,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAOlH,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;;IAe3B,aAAa,IAAI,IAAI;CAqDtB"}
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;CA4EtB"}
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 from './route-matching.js';
4
+ import applyRouteMatching, { 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';
@@ -28,10 +28,23 @@ export default class Request {
28
28
  constructor() {
29
29
  const api = express();
30
30
  api.disable('x-powered-by');
31
- // Closes sub-paths (/public/SUCCESS) for abofs/stonyx-rest-server#47.
32
- // Must stay in the constructor: registerCalls() materializes this router,
33
- // and a set applied afterwards has no effect. The parent app's setting
34
- // does not reach here -- see src/route-matching.ts.
31
+ // Applies BOTH route-matching SETTINGS: case sensitive routing
32
+ // (abofs/stonyx-rest-server#47) and strict routing (#50). For #47 this
33
+ // call closes sub-paths (/public/SUCCESS) and the parent's call closes the
34
+ // mount segment; for #50 THIS call closes the entire trailing-slash
35
+ // authorization bypass on its own, and the parent's call has no security
36
+ // role. Must stay in the constructor: registerCalls() materializes this
37
+ // router, and a set applied afterwards has no effect. The parent app's
38
+ // setting does not reach here -- see src/route-matching.ts.
39
+ //
40
+ // The third route-matching control, `canonicalRoutes` (#54), is NOT applied
41
+ // here and must not be moved here. It is not an express setting, and its
42
+ // timing contract is the opposite of these two: it is read PER REQUEST
43
+ // inside the handler closure in registerCalls() below, via
44
+ // shouldRejectTarget(). These two are constructor-timed because a late
45
+ // `set` is silently ineffective; that hazard does not exist for #54, and
46
+ // reading it per request is what lets the unit AC flip the flag between
47
+ // probes.
35
48
  applyRouteMatching(api);
36
49
  this.expressInstance = api;
37
50
  }
@@ -44,7 +57,30 @@ export default class Request {
44
57
  continue;
45
58
  }
46
59
  for (const [route, handler] of Object.entries(handlers)) {
47
- expressInstance[method](route, async (req, res) => {
60
+ expressInstance[method](route, async (req, res, next) => {
61
+ // abofs/stonyx-rest-server#54 -- reject a request whose RAW target is
62
+ // not the canonical path express matched, closing two authorization
63
+ // bypasses against hooks that authorize on `req.originalUrl`: the
64
+ // mount-root trailing slash and the absolute-form request target.
65
+ //
66
+ // Two properties of this line are load-bearing, and each has its own
67
+ // red-able assertion:
68
+ //
69
+ // 1. It runs BEFORE auth, and OUTSIDE `if (this.auth)`. Gating it on
70
+ // the hook would leave `GET /public/` at 200 and make a security
71
+ // control depend on an unrelated consumer choice.
72
+ // 2. It rejects with `next('router')`, NOT sendStatusResponse() or
73
+ // res.sendStatus(404). Measured: sendStatus returns
74
+ // `text/plain "Not Found"` while a genuine miss returns
75
+ // `text/html <pre>Cannot GET ...</pre>` -- a working ORACLE
76
+ // telling an attacker the route exists but was spelled wrong.
77
+ // next('router') exits this sub-app's router into finalhandler
78
+ // and is shape-identical to a real miss (same status, same
79
+ // Content-Type, same CSP header). Routing it through
80
+ // sendStatusResponse() would additionally re-introduce the
81
+ // oracle for any consumer who sets a 404 in `statusMap`.
82
+ if (shouldRejectTarget(req))
83
+ return next('router');
48
84
  // Run auth after route matching so request.params is populated
49
85
  if (this.auth) {
50
86
  const status = this.auth(req, getState(req));
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { Express } from 'express';
1
+ import type { Express, Request as ExpressRequest } from 'express';
2
2
  /**
3
3
  * Applies this module's route-matching settings to an express app.
4
4
  *
@@ -53,25 +53,91 @@ import type { Express } from 'express';
53
53
  * this repo: `/health/`, the only route registered directly on the parent
54
54
  * app. It has no security role here; do not describe it as having one.
55
55
  *
56
- * The cause is concrete: router@2.2.0 `index.js:400-401` hardcodes
57
- * `strict: false, end: false` for `Router.prototype.use`, so mount segments
58
- * are structurally strict-immune. That is the OPPOSITE of `sensitive`, which
59
- * `use()` does forward (line 399) and which is why #47's parent site closed
60
- * `/PUBLIC/...`. Both sites still get both settings -- they share this
61
- * function -- but the justification differs and the tests are built on the
62
- * measured split, not on the analogy.
63
- *
64
- * Consequence worth stating so nobody "fixes" it: the mount-segment trailing
65
- * slash (`/public/`) can never be closed by this setting, and does not need to
66
- * be. For both `/public` and `/public/` the mounted sub-app receives
67
- * `req.path === '/'`, so a `req.path` auth hook sees no difference and there is
68
- * no asymmetry. A test asserting `/public/` -> 404 could never pass. The
69
- * residual is `req.originalUrl`, which DOES differ; that is disclosed in the
70
- * README rather than silently closed over.
56
+ * The cause is concrete: `Router.prototype.use` hardcodes `strict: false` (and
57
+ * `end: false`), so mount segments are structurally strict-immune. That is the
58
+ * OPPOSITE of `sensitive`, which `use()` DOES forward, and which is why #47's
59
+ * parent site closed `/PUBLIC/...`. The version-pinned file-and-line citation
60
+ * for that upstream behaviour is deliberately kept in ONE place --
61
+ * `docs/project-structure.md`, section "Strict routing (#50)" -- so a router
62
+ * upgrade invalidates one line rather than five. Both sites still get both
63
+ * settings (they share this function), but the justification differs and the
64
+ * tests are built on the measured split, not on the analogy.
65
+ *
66
+ * Consequence worth stating so nobody expects this SETTING to cover it: no
67
+ * express setting rejects the mount-segment trailing slash (`/public/`). For
68
+ * both `/public` and `/public/` the mounted sub-app receives `req.path === '/'`,
69
+ * so a `req.path` auth hook sees no difference and there is nothing for the
70
+ * setting to reject. That statement is still true, and it is still the reason
71
+ * `applyRouteMatching()` is not the fix site for that edge.
72
+ *
73
+ * The edge itself is CLOSED, by `shouldRejectTarget()` below rather than by a
74
+ * setting (abofs/stonyx-rest-server#54). `req.originalUrl` does differ between
75
+ * the two spellings, and a hook authorizing on it was bypassed by one
76
+ * character -- measured: `GET /admin` -> 401, `GET /admin/` -> 200 with the
77
+ * guarded handler running unauthenticated. `GET /public/` now returns 404, and
78
+ * the integration AC asserts exactly that.
71
79
  *
72
80
  * Both guards are `!== false` for the same reason: these flags default to the
73
81
  * truthy direction, so a truthy check fails OPEN for a consumer whose shipped
74
- * config predates the key.
82
+ * config predates the key. Both are also asserted at the unit tier for BOTH
83
+ * 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). The
85
+ * integration tier cannot see either: with the shipped default `true`, a
86
+ * fail-open guard leaves every integration assertion green.
75
87
  */
76
88
  export default function applyRouteMatching(api: Express): void;
89
+ /**
90
+ * Decides whether a request must be rejected because its RAW request target is
91
+ * not the canonical path express matched (abofs/stonyx-rest-server#54).
92
+ *
93
+ * Closes two live authorization bypasses against a consumer hook that
94
+ * authorizes on `req.originalUrl` -- the field express does NOT normalize:
95
+ *
96
+ * 1. mount-root trailing slash GET /admin/ -> was 200
97
+ * 2. absolute-form request target GET http://host/admin -> was 200
98
+ * (RFC 9112 3.2.2; hits EVERY route, not just the mount root)
99
+ *
100
+ * Both were measured reaching the guarded handler unauthenticated while
101
+ * `GET /admin` was denied 401.
102
+ *
103
+ * COMPARE THE RAW TARGET; DO NOT PARSE IT. Any implementation reaching for
104
+ * `new URL(req.originalUrl, base).pathname` to "get the path" re-opens vector 2
105
+ * BY CONSTRUCTION: parsing normalizes the exact string the consumer's hook is
106
+ * exposed to, so the check would compare a laundered value while the hook still
107
+ * sees the raw one. Measured: the narrow `endsWith('/')` form closes 1 and
108
+ * leaves 2 at 200.
109
+ *
110
+ * `req.baseUrl + req.path` is likewise NOT usable as the left-hand side. It is
111
+ * `/admin/` for BOTH spellings of vector 1 -- `originalUrl` is the only field
112
+ * that differs, which is precisely why the bypass exists. A check built on
113
+ * `baseUrl + path` cannot see its own defect.
114
+ *
115
+ * TIMING CONTRACT -- DELIBERATELY DIFFERENT FROM ITS TWO SIBLINGS. This lives
116
+ * beside `applyRouteMatching()` for the same "one place anchors it" reason, but
117
+ * deliberately OUTSIDE it: that function's contract is *apply express settings
118
+ * to an app*, it is called from two constructors, and this is neither a setting
119
+ * nor constructor-timed. `caseSensitiveRoutes`/`strictRoutes` are read once in a
120
+ * constructor and are silently ineffective if applied late; this flag is read
121
+ * PER REQUEST, inside the handler closure. There is no lazy-materialisation
122
+ * hazard here, so do not carry that constraint across.
123
+ *
124
+ * The caller must reject with `next('router')`, NOT `res.sendStatus(404)`, and
125
+ * must run this whether or not `this.auth` is set -- see src/request.ts.
126
+ *
127
+ * Guard polarity is `!== false`, matching both siblings, for the same measured
128
+ * reason: the secure value is the TRUTHY one, so a plain truthy check fails
129
+ * OPEN for any consumer whose shipped `restServer` block predates the key --
130
+ * the state every existing consumer is in, and reachable in practice because
131
+ * the stonyx loader only merges a module's `config/environment.js` for modules
132
+ * in devDependencies. `trustProxy` deliberately differs (`=== 'true'`): its safe
133
+ * default is FALSY, so a truthy check already fails closed for it. The rule is
134
+ * "the guard must fail toward the safe value", not "all guards look alike" --
135
+ * preserve the asymmetry.
136
+ *
137
+ * The integration tier cannot see a fail-open guard here: with the shipped
138
+ * default `true`, `=== true` leaves every integration assertion green. Only
139
+ * `test/unit/request-test.ts` AC2 can, and it probes BOTH failure shapes --
140
+ * key present-and-`undefined` and key absent as an own property.
141
+ */
142
+ export declare function shouldRejectTarget(req: ExpressRequest): boolean;
77
143
  //# 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,MAAM,SAAS,CAAC;AAGvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG7D"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAG7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAe/D"}
@@ -53,25 +53,37 @@ import config from 'stonyx/config';
53
53
  * this repo: `/health/`, the only route registered directly on the parent
54
54
  * app. It has no security role here; do not describe it as having one.
55
55
  *
56
- * The cause is concrete: router@2.2.0 `index.js:400-401` hardcodes
57
- * `strict: false, end: false` for `Router.prototype.use`, so mount segments
58
- * are structurally strict-immune. That is the OPPOSITE of `sensitive`, which
59
- * `use()` does forward (line 399) and which is why #47's parent site closed
60
- * `/PUBLIC/...`. Both sites still get both settings -- they share this
61
- * function -- but the justification differs and the tests are built on the
62
- * measured split, not on the analogy.
63
- *
64
- * Consequence worth stating so nobody "fixes" it: the mount-segment trailing
65
- * slash (`/public/`) can never be closed by this setting, and does not need to
66
- * be. For both `/public` and `/public/` the mounted sub-app receives
67
- * `req.path === '/'`, so a `req.path` auth hook sees no difference and there is
68
- * no asymmetry. A test asserting `/public/` -> 404 could never pass. The
69
- * residual is `req.originalUrl`, which DOES differ; that is disclosed in the
70
- * README rather than silently closed over.
56
+ * The cause is concrete: `Router.prototype.use` hardcodes `strict: false` (and
57
+ * `end: false`), so mount segments are structurally strict-immune. That is the
58
+ * OPPOSITE of `sensitive`, which `use()` DOES forward, and which is why #47's
59
+ * parent site closed `/PUBLIC/...`. The version-pinned file-and-line citation
60
+ * for that upstream behaviour is deliberately kept in ONE place --
61
+ * `docs/project-structure.md`, section "Strict routing (#50)" -- so a router
62
+ * upgrade invalidates one line rather than five. Both sites still get both
63
+ * settings (they share this function), but the justification differs and the
64
+ * tests are built on the measured split, not on the analogy.
65
+ *
66
+ * Consequence worth stating so nobody expects this SETTING to cover it: no
67
+ * express setting rejects the mount-segment trailing slash (`/public/`). For
68
+ * both `/public` and `/public/` the mounted sub-app receives `req.path === '/'`,
69
+ * so a `req.path` auth hook sees no difference and there is nothing for the
70
+ * setting to reject. That statement is still true, and it is still the reason
71
+ * `applyRouteMatching()` is not the fix site for that edge.
72
+ *
73
+ * The edge itself is CLOSED, by `shouldRejectTarget()` below rather than by a
74
+ * setting (abofs/stonyx-rest-server#54). `req.originalUrl` does differ between
75
+ * the two spellings, and a hook authorizing on it was bypassed by one
76
+ * character -- measured: `GET /admin` -> 401, `GET /admin/` -> 200 with the
77
+ * guarded handler running unauthenticated. `GET /public/` now returns 404, and
78
+ * the integration AC asserts exactly that.
71
79
  *
72
80
  * Both guards are `!== false` for the same reason: these flags default to the
73
81
  * truthy direction, so a truthy check fails OPEN for a consumer whose shipped
74
- * config predates the key.
82
+ * config predates the key. Both are also asserted at the unit tier for BOTH
83
+ * 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). The
85
+ * integration tier cannot see either: with the shipped default `true`, a
86
+ * fail-open guard leaves every integration assertion green.
75
87
  */
76
88
  export default function applyRouteMatching(api) {
77
89
  if (config.restServer?.caseSensitiveRoutes !== false)
@@ -79,4 +91,71 @@ export default function applyRouteMatching(api) {
79
91
  if (config.restServer?.strictRoutes !== false)
80
92
  api.set('strict routing', true);
81
93
  }
94
+ /**
95
+ * Decides whether a request must be rejected because its RAW request target is
96
+ * not the canonical path express matched (abofs/stonyx-rest-server#54).
97
+ *
98
+ * Closes two live authorization bypasses against a consumer hook that
99
+ * authorizes on `req.originalUrl` -- the field express does NOT normalize:
100
+ *
101
+ * 1. mount-root trailing slash GET /admin/ -> was 200
102
+ * 2. absolute-form request target GET http://host/admin -> was 200
103
+ * (RFC 9112 3.2.2; hits EVERY route, not just the mount root)
104
+ *
105
+ * Both were measured reaching the guarded handler unauthenticated while
106
+ * `GET /admin` was denied 401.
107
+ *
108
+ * COMPARE THE RAW TARGET; DO NOT PARSE IT. Any implementation reaching for
109
+ * `new URL(req.originalUrl, base).pathname` to "get the path" re-opens vector 2
110
+ * BY CONSTRUCTION: parsing normalizes the exact string the consumer's hook is
111
+ * exposed to, so the check would compare a laundered value while the hook still
112
+ * sees the raw one. Measured: the narrow `endsWith('/')` form closes 1 and
113
+ * leaves 2 at 200.
114
+ *
115
+ * `req.baseUrl + req.path` is likewise NOT usable as the left-hand side. It is
116
+ * `/admin/` for BOTH spellings of vector 1 -- `originalUrl` is the only field
117
+ * that differs, which is precisely why the bypass exists. A check built on
118
+ * `baseUrl + path` cannot see its own defect.
119
+ *
120
+ * TIMING CONTRACT -- DELIBERATELY DIFFERENT FROM ITS TWO SIBLINGS. This lives
121
+ * beside `applyRouteMatching()` for the same "one place anchors it" reason, but
122
+ * deliberately OUTSIDE it: that function's contract is *apply express settings
123
+ * to an app*, it is called from two constructors, and this is neither a setting
124
+ * nor constructor-timed. `caseSensitiveRoutes`/`strictRoutes` are read once in a
125
+ * constructor and are silently ineffective if applied late; this flag is read
126
+ * PER REQUEST, inside the handler closure. There is no lazy-materialisation
127
+ * hazard here, so do not carry that constraint across.
128
+ *
129
+ * The caller must reject with `next('router')`, NOT `res.sendStatus(404)`, and
130
+ * must run this whether or not `this.auth` is set -- see src/request.ts.
131
+ *
132
+ * Guard polarity is `!== false`, matching both siblings, for the same measured
133
+ * reason: the secure value is the TRUTHY one, so a plain truthy check fails
134
+ * OPEN for any consumer whose shipped `restServer` block predates the key --
135
+ * the state every existing consumer is in, and reachable in practice because
136
+ * the stonyx loader only merges a module's `config/environment.js` for modules
137
+ * in devDependencies. `trustProxy` deliberately differs (`=== 'true'`): its safe
138
+ * default is FALSY, so a truthy check already fails closed for it. The rule is
139
+ * "the guard must fail toward the safe value", not "all guards look alike" --
140
+ * preserve the asymmetry.
141
+ *
142
+ * The integration tier cannot see a fail-open guard here: with the shipped
143
+ * default `true`, `=== true` leaves every integration assertion green. Only
144
+ * `test/unit/request-test.ts` AC2 can, and it probes BOTH failure shapes --
145
+ * key present-and-`undefined` and key absent as an own property.
146
+ */
147
+ export function shouldRejectTarget(req) {
148
+ // Written as `!== false` rather than `=== false` on purpose: the polarity is
149
+ // the load-bearing part and it should read identically to the two guards in
150
+ // applyRouteMatching() above.
151
+ const enforced = config.restServer?.canonicalRoutes !== false;
152
+ if (!enforced)
153
+ return false;
154
+ // Raw, unparsed. Only the query string is removed, by string split.
155
+ const target = req.originalUrl.split('?')[0];
156
+ // At a mount root express reports `req.path === '/'` while the canonical
157
+ // target is the bare mount segment, so the two are not simply concatenated.
158
+ const canonical = req.path === '/' && req.baseUrl ? req.baseUrl : req.baseUrl + req.path;
159
+ return target !== canonical;
160
+ }
82
161
  //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;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"}
1
+ {"version":3,"file":"route-matching.js","sourceRoot":"","sources":["../src/route-matching.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;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,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"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-alpha.22",
7
+ "version": "0.2.1-alpha.24",
8
8
  "description": "Rest Server Module for Stonyx Framework",
9
9
  "repository": {
10
10
  "type": "git",