@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 +59 -19
- package/dist/main.js +1 -1
- package/dist/request.js +1 -1
- package/dist/route-matching.d.ts +1 -1
- package/dist/route-matching.js +1 -1
- package/package.json +1 -1
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)
|
|
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
|
|
89
|
-
at `/users`; `GET /Users`
|
|
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
|
|
104
|
-
sees can only ever be the exact registered casing.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
is
|
|
117
|
-
|
|
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**
|
|
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
|
|
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()
|
|
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);
|
package/dist/route-matching.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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/dist/route-matching.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|