@stonyx/rest-server 0.2.1-alpha.19 → 0.2.1-alpha.20

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
@@ -79,14 +79,20 @@ Configuration is read from `stonyx/config` under `restServer`:
79
79
  | `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
80
80
  | `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
81
81
  | `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
82
- | `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Case-Sensitive Routing](#case-sensitive-routing) -- **disabling this re-opens a security hole**. |
82
+ | `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Disable via `REST_CASE_SENSITIVE_ROUTES=false`. See [Case-Sensitive Routing](#case-sensitive-routing) **disabling this re-opens a security hole**. |
83
83
  | `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. |
84
84
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
85
85
 
86
86
  ### Case-Sensitive Routing
87
87
 
88
- Routes match **case-sensitively by default**. `GET /users` reaches a route mounted
89
- at `/users`; `GET /Users` and `GET /users/ID` do not.
88
+ Routes match **case-sensitively by default**. `GET /users` reaches a route
89
+ mounted at `/users`; `GET /Users` does not reach that mount, and
90
+ `GET /users/Success` does not reach a `/success` handler registered inside it.
91
+
92
+ Read [What this does not do](#what-this-does-not-do) before you rely on that
93
+ sentence. Two things it does not say: "does not reach the handler" is not the
94
+ same as "404", and casing is only one of the two ways express matches more
95
+ loosely than the authorization predicates written against it.
90
96
 
91
97
  This is deliberate and security-relevant. Express matches case-insensitively by
92
98
  default, which means any authorization written against the request URL can be
@@ -100,21 +106,55 @@ DELETE /ANIMALS/22 -> 204 (record destroyed)
100
106
 
101
107
  The consumer's predicate is stricter than the router that dispatched the
102
108
  request, so the router hands the handler a request the predicate would have
103
- rejected. Case-sensitive matching removes that asymmetry: the path a handler
104
- sees can only ever be the exact registered casing.
105
-
106
- **What this does not do.** Case-sensitive routing does not normalise path
107
- *parameter values*. If your `auth()` hook rejects `params.id === 'restricted'`,
108
- then `GET /private/RESTRICTED` still reaches the handler -- the router matched
109
- the route correctly and `restricted` and `RESTRICTED` are different values.
110
- Record ids are legitimately case-sensitive, so this is a comparison your
111
- application owns. Compare param values with the same case-handling you use when
112
- you look them up.
113
-
114
- The framework also does not redirect or rewrite mixed-case requests to their
115
- canonical casing. Whether `/Users` is a typo to forgive or an attack to reject
116
- is an application policy decision, and encoding it here would mint another
117
- variant of the bug above.
109
+ rejected. Case-sensitive matching closes the **casing** half of that asymmetry:
110
+ the path a handler sees can only ever be the exact registered casing.
111
+
112
+ It does not close the asymmetry itself. Express exposes `case sensitive
113
+ routing` and `strict routing` as a pair of loose-by-default router settings and
114
+ this change sets only the first, so the identical bypass is still reachable by
115
+ appending a slash. Measured on this release against this repo's own fixture:
116
+
117
+ ```
118
+ GET /private/failure -> 505 (auth hook fires, request blocked)
119
+ GET /private/failure/ -> 200 (auth hook never fires, handler runs)
120
+ ```
121
+
122
+ That is the same defect, one character instead of a case shift — translated to
123
+ the example above, `DELETE /animals/22` is filtered and `DELETE /animals/22/`
124
+ destroys the record. It is tracked as
125
+ [#50](https://github.com/abofs/stonyx-rest-server/issues/50) and is not fixed
126
+ here; it is a second consumer-visible behaviour change that needs its own flag
127
+ and its own release note.
128
+
129
+ **So do not drop a URL-normalizing defence you already have on the strength of
130
+ this section.** If your authorization compares `req.path` or `req.originalUrl`,
131
+ keep whatever normalization you have until #50 ships.
132
+
133
+ #### What this does not do
134
+
135
+ **It does not normalize path *parameter values*.** If your `auth()` hook rejects
136
+ `params.id === 'restricted'`, then `GET /private/RESTRICTED` still reaches the
137
+ handler — the router matched the route correctly, and `restricted` and
138
+ `RESTRICTED` are different values. Record ids are legitimately case-sensitive,
139
+ so this is a comparison your application owns. Compare param values with the
140
+ same case-handling you use when you look them up.
141
+
142
+ **A sub-path that misses is not necessarily a 404.** If the route class also
143
+ registers a param route such as `/:id`, a mis-cased sub-path is absorbed by it
144
+ rather than rejected. `GET /private/FAILURE` misses `/failure` and is dispatched
145
+ to `/:id` with `id="FAILURE"` — a different handler, at 200, not a miss; this
146
+ repo's AC5 asserts exactly that. A class exposing `/orders/summary` alongside
147
+ `/orders/:id` will send `GET /orders/SUMMARY` into the `/:id` handler and its
148
+ database lookup. The param route's own `auth()` hook still runs, so this is an
149
+ expectation defect rather than a bypass — but plan for a reroute, not a 404.
150
+
151
+ **It does not cover trailing slashes.** See
152
+ [#50](https://github.com/abofs/stonyx-rest-server/issues/50) above.
153
+
154
+ **It does not redirect or rewrite** mixed-case requests to their canonical
155
+ casing. Whether `/Users` is a typo to forgive or an attack to reject is an
156
+ application policy decision, and encoding it here would mint another variant of
157
+ the bug above.
118
158
 
119
159
  #### Opting out
120
160
 
@@ -122,7 +162,7 @@ variant of the bug above.
122
162
  REST_CASE_SENSITIVE_ROUTES=false
123
163
  ```
124
164
 
125
- **This restores the vulnerability described above** -- any URL-based
165
+ **This restores the vulnerability described above** any URL-based
126
166
  authorization in your application becomes bypassable by changing case. It
127
167
  exists as a one-line remediation for an existing deployment, not as a
128
168
  configuration to run on.
package/dist/main.js CHANGED
@@ -30,7 +30,7 @@ export default class RestServer {
30
30
  RestServer.instance = this;
31
31
  this.api = express();
32
32
  // Closes the mount segment (/PUBLIC/...) for abofs/stonyx-rest-server#47.
33
- // Must stay in the constructor: the router is materialised lazily on first
33
+ // Must stay in the constructor: the router is materialized lazily on first
34
34
  // route registration, so applying this after setupRouter() is silently
35
35
  // ineffective. The matching call in Request's constructor is what closes
36
36
  // sub-paths -- see src/route-matching.ts for why both are required.
package/dist/request.js CHANGED
@@ -29,7 +29,7 @@ export default class Request {
29
29
  const api = express();
30
30
  api.disable('x-powered-by');
31
31
  // Closes sub-paths (/public/SUCCESS) for abofs/stonyx-rest-server#47.
32
- // Must stay in the constructor: registerCalls() materialises this router,
32
+ // Must stay in the constructor: registerCalls() materializes this router,
33
33
  // and a set applied afterwards has no effect. The parent app's setting
34
34
  // does not reach here -- see src/route-matching.ts.
35
35
  applyRouteMatching(api);
@@ -10,7 +10,7 @@ import type { Express } from 'express';
10
10
  * by the time the parent's setting could reach it.
11
11
  *
12
12
  * Both callers invoke this from a constructor, and must keep doing so: express
13
- * materialises a router lazily on first route registration, and a setting
13
+ * materializes a router lazily on first route registration, and a setting
14
14
  * applied afterwards is silently ineffective -- no throw, no warning.
15
15
  *
16
16
  * The guard is `!== false`, not a plain truthy check, and that polarity is
@@ -10,7 +10,7 @@ import config from 'stonyx/config';
10
10
  * by the time the parent's setting could reach it.
11
11
  *
12
12
  * Both callers invoke this from a constructor, and must keep doing so: express
13
- * materialises a router lazily on first route registration, and a setting
13
+ * materializes a router lazily on first route registration, and a setting
14
14
  * applied afterwards is silently ineffective -- no throw, no warning.
15
15
  *
16
16
  * The guard is `!== false`, not a plain truthy check, and that polarity is
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-alpha.19",
7
+ "version": "0.2.1-alpha.20",
8
8
  "description": "Rest Server Module for Stonyx Framework",
9
9
  "repository": {
10
10
  "type": "git",