@remix-run/csrf-middleware 0.1.2 → 0.1.4
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 +5 -5
- package/dist/lib/csrf.d.ts +7 -6
- package/dist/lib/csrf.d.ts.map +1 -1
- package/dist/lib/csrf.js +10 -6
- package/package.json +10 -10
- package/src/lib/csrf.ts +28 -16
package/README.md
CHANGED
|
@@ -21,10 +21,10 @@ This middleware requires [`session-middleware`](https://github.com/remix-run/rem
|
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
23
|
import { createCookie } from 'remix/cookie'
|
|
24
|
-
import { createRouter } from 'remix/
|
|
25
|
-
import { createCookieSessionStorage } from 'remix/session/cookie
|
|
26
|
-
import { session } from 'remix/session
|
|
27
|
-
import { csrf, getCsrfToken } from 'remix/csrf
|
|
24
|
+
import { createRouter } from 'remix/router'
|
|
25
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
26
|
+
import { session } from 'remix/middleware/session'
|
|
27
|
+
import { csrf, getCsrfToken } from 'remix/middleware/csrf'
|
|
28
28
|
|
|
29
29
|
let sessionCookie = createCookie('__session', { secrets: ['secret1'] })
|
|
30
30
|
let sessionStorage = createCookieSessionStorage()
|
|
@@ -49,7 +49,7 @@ router.get('/form', (context) => {
|
|
|
49
49
|
|
|
50
50
|
By default, `csrf()` checks token values in this order:
|
|
51
51
|
|
|
52
|
-
1. Request headers: `
|
|
52
|
+
1. Request headers: `X-Csrf-Token`, `X-Xsrf-Token`, `Csrf-Token`
|
|
53
53
|
2. Form field: `_csrf` (requires `formData()` middleware to parse request bodies)
|
|
54
54
|
3. Query param: `_csrf`
|
|
55
55
|
|
package/dist/lib/csrf.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router';
|
|
1
|
+
import type { AnyParams, ContextEntries, Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router';
|
|
2
|
+
type AnyRequestContext = RequestContext<AnyParams, ContextEntries>;
|
|
2
3
|
type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>;
|
|
3
4
|
/**
|
|
4
5
|
* Return shape for a dynamic CSRF origin resolver.
|
|
@@ -11,7 +12,7 @@ export interface CsrfOriginResolver {
|
|
|
11
12
|
/**
|
|
12
13
|
* Resolves whether an unsafe request origin should be trusted.
|
|
13
14
|
*/
|
|
14
|
-
(origin: string, context:
|
|
15
|
+
(origin: string, context: AnyRequestContext): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Accepted forms for configuring allowed CSRF origins.
|
|
@@ -28,7 +29,7 @@ export interface CsrfTokenResolver {
|
|
|
28
29
|
/**
|
|
29
30
|
* Resolves the submitted CSRF token for the current request.
|
|
30
31
|
*/
|
|
31
|
-
(context:
|
|
32
|
+
(context: AnyRequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* The reason a CSRF request was rejected.
|
|
@@ -53,7 +54,7 @@ export interface CsrfOptions {
|
|
|
53
54
|
/**
|
|
54
55
|
* Header names checked (in order) for CSRF tokens.
|
|
55
56
|
*
|
|
56
|
-
* @default ['
|
|
57
|
+
* @default ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
|
|
57
58
|
*/
|
|
58
59
|
headerNames?: readonly string[];
|
|
59
60
|
/**
|
|
@@ -81,7 +82,7 @@ export interface CsrfOptions {
|
|
|
81
82
|
/**
|
|
82
83
|
* Optional custom error response for rejected requests.
|
|
83
84
|
*/
|
|
84
|
-
onError?: (reason: CsrfFailureReason, context:
|
|
85
|
+
onError?: (reason: CsrfFailureReason, context: AnyRequestContext) => Response | Promise<Response>;
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
87
88
|
* Session-backed CSRF protection middleware.
|
|
@@ -99,6 +100,6 @@ export declare function csrf(options?: CsrfOptions): Middleware;
|
|
|
99
100
|
* @param tokenKey Session key that stores the token
|
|
100
101
|
* @returns The active CSRF token
|
|
101
102
|
*/
|
|
102
|
-
export declare function getCsrfToken(context:
|
|
103
|
+
export declare function getCsrfToken(context: AnyRequestContext, tokenKey?: string): string;
|
|
103
104
|
export {};
|
|
104
105
|
//# sourceMappingURL=csrf.d.ts.map
|
package/dist/lib/csrf.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/lib/csrf.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/lib/csrf.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,UAAU,EACV,cAAc,EACd,aAAa,EACd,MAAM,yBAAyB,CAAA;AAMhC,KAAK,iBAAiB,GAAG,cAAc,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;AAClE,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;AAErE;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,CACE,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,iBAAiB,GACzB,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;CAChE;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAE3D;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;AAE/D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,CAAC,OAAO,EAAE,iBAAiB,GAAG,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CACzF;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,eAAe,CAAA;AAEpF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;IAEtC;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;IAEnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,iBAAiB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAClG;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAwC1D;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,SAAU,GAAG,MAAM,CAenF"}
|
package/dist/lib/csrf.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isRequestMethod } from '@remix-run/fetch-router';
|
|
1
2
|
import { Session } from '@remix-run/session';
|
|
2
3
|
const defaultSafeMethods = ['GET', 'HEAD', 'OPTIONS'];
|
|
3
|
-
const defaultTokenHeaderNames = ['
|
|
4
|
+
const defaultTokenHeaderNames = ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token'];
|
|
4
5
|
/**
|
|
5
6
|
* Session-backed CSRF protection middleware.
|
|
6
7
|
*
|
|
@@ -16,11 +17,11 @@ export function csrf(options = {}) {
|
|
|
16
17
|
let headerNames = options.headerNames ?? defaultTokenHeaderNames;
|
|
17
18
|
let allowMissingOrigin = options.allowMissingOrigin ?? true;
|
|
18
19
|
return async (context, next) => {
|
|
19
|
-
if (
|
|
20
|
+
if (context.get(Session) == null) {
|
|
20
21
|
throw new Error('csrf middleware requires session() middleware to run before it');
|
|
21
22
|
}
|
|
22
23
|
let expectedToken = getCsrfToken(context, tokenKey);
|
|
23
|
-
if (
|
|
24
|
+
if (isSafeMethod(context.method, safeMethods)) {
|
|
24
25
|
return next();
|
|
25
26
|
}
|
|
26
27
|
let validOrigin = await validateRequestOrigin(context, options.origin, allowMissingOrigin, context.url.origin);
|
|
@@ -37,6 +38,9 @@ export function csrf(options = {}) {
|
|
|
37
38
|
return next();
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function isSafeMethod(method, safeMethods) {
|
|
42
|
+
return isRequestMethod(method) && safeMethods.includes(method);
|
|
43
|
+
}
|
|
40
44
|
/**
|
|
41
45
|
* Gets the CSRF token from the session. Creates one if missing.
|
|
42
46
|
*
|
|
@@ -45,10 +49,10 @@ export function csrf(options = {}) {
|
|
|
45
49
|
* @returns The active CSRF token
|
|
46
50
|
*/
|
|
47
51
|
export function getCsrfToken(context, tokenKey = '_csrf') {
|
|
48
|
-
|
|
52
|
+
let session = context.get(Session);
|
|
53
|
+
if (session == null) {
|
|
49
54
|
throw new Error('Session is not started. Use session() middleware before csrf().');
|
|
50
55
|
}
|
|
51
|
-
let session = context.get(Session);
|
|
52
56
|
let token = session.get(tokenKey);
|
|
53
57
|
if (typeof token === 'string' && token !== '') {
|
|
54
58
|
return token;
|
|
@@ -97,7 +101,7 @@ async function resolveSubmittedToken(context, valueResolver, fieldName, headerNa
|
|
|
97
101
|
return trimmedHeaderValue;
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
|
-
let formValue = context.
|
|
104
|
+
let formValue = context.get(FormData)?.get(fieldName);
|
|
101
105
|
if (typeof formValue === 'string') {
|
|
102
106
|
let trimmedFormValue = formValue.trim();
|
|
103
107
|
if (trimmedFormValue !== '') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/csrf-middleware",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Middleware for CSRF protection in Fetch API servers",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.6.0",
|
|
30
30
|
"@typescript/native-preview": "7.0.0-dev.20251125.1",
|
|
31
|
-
"@remix-run/
|
|
32
|
-
"@remix-run/
|
|
33
|
-
"@remix-run/
|
|
34
|
-
"@remix-run/session": "0.4.
|
|
35
|
-
"@remix-run/
|
|
36
|
-
"@remix-run/
|
|
37
|
-
"@remix-run/
|
|
31
|
+
"@remix-run/assert": "0.2.1",
|
|
32
|
+
"@remix-run/form-data-middleware": "0.3.1",
|
|
33
|
+
"@remix-run/cookie": "0.5.3",
|
|
34
|
+
"@remix-run/session": "0.4.2",
|
|
35
|
+
"@remix-run/fetch-router": "0.19.1",
|
|
36
|
+
"@remix-run/session-middleware": "0.3.1",
|
|
37
|
+
"@remix-run/test": "0.4.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@remix-run/fetch-router": "^0.
|
|
41
|
-
"@remix-run/session": "^0.4.
|
|
40
|
+
"@remix-run/fetch-router": "^0.19.1",
|
|
41
|
+
"@remix-run/session": "^0.4.2"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"fetch",
|
package/src/lib/csrf.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isRequestMethod } from '@remix-run/fetch-router'
|
|
2
|
+
import type {
|
|
3
|
+
AnyParams,
|
|
4
|
+
ContextEntries,
|
|
5
|
+
Middleware,
|
|
6
|
+
RequestContext,
|
|
7
|
+
RequestMethod,
|
|
8
|
+
} from '@remix-run/fetch-router'
|
|
2
9
|
import { Session } from '@remix-run/session'
|
|
3
10
|
|
|
4
11
|
const defaultSafeMethods: RequestMethod[] = ['GET', 'HEAD', 'OPTIONS']
|
|
5
|
-
const defaultTokenHeaderNames = ['
|
|
12
|
+
const defaultTokenHeaderNames = ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
|
|
6
13
|
|
|
14
|
+
type AnyRequestContext = RequestContext<AnyParams, ContextEntries>
|
|
7
15
|
type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>
|
|
8
16
|
|
|
9
17
|
/**
|
|
@@ -20,7 +28,7 @@ export interface CsrfOriginResolver {
|
|
|
20
28
|
*/
|
|
21
29
|
(
|
|
22
30
|
origin: string,
|
|
23
|
-
context:
|
|
31
|
+
context: AnyRequestContext,
|
|
24
32
|
): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>
|
|
25
33
|
}
|
|
26
34
|
|
|
@@ -41,7 +49,7 @@ export interface CsrfTokenResolver {
|
|
|
41
49
|
/**
|
|
42
50
|
* Resolves the submitted CSRF token for the current request.
|
|
43
51
|
*/
|
|
44
|
-
(context:
|
|
52
|
+
(context: AnyRequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
@@ -70,7 +78,7 @@ export interface CsrfOptions {
|
|
|
70
78
|
/**
|
|
71
79
|
* Header names checked (in order) for CSRF tokens.
|
|
72
80
|
*
|
|
73
|
-
* @default ['
|
|
81
|
+
* @default ['X-Csrf-Token', 'X-Xsrf-Token', 'Csrf-Token']
|
|
74
82
|
*/
|
|
75
83
|
headerNames?: readonly string[]
|
|
76
84
|
|
|
@@ -103,7 +111,7 @@ export interface CsrfOptions {
|
|
|
103
111
|
/**
|
|
104
112
|
* Optional custom error response for rejected requests.
|
|
105
113
|
*/
|
|
106
|
-
onError?: (reason: CsrfFailureReason, context:
|
|
114
|
+
onError?: (reason: CsrfFailureReason, context: AnyRequestContext) => Response | Promise<Response>
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -122,13 +130,13 @@ export function csrf(options: CsrfOptions = {}): Middleware {
|
|
|
122
130
|
let allowMissingOrigin = options.allowMissingOrigin ?? true
|
|
123
131
|
|
|
124
132
|
return async (context, next) => {
|
|
125
|
-
if (
|
|
133
|
+
if (context.get(Session) == null) {
|
|
126
134
|
throw new Error('csrf middleware requires session() middleware to run before it')
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
let expectedToken = getCsrfToken(context, tokenKey)
|
|
130
138
|
|
|
131
|
-
if (
|
|
139
|
+
if (isSafeMethod(context.method, safeMethods)) {
|
|
132
140
|
return next()
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -156,6 +164,10 @@ export function csrf(options: CsrfOptions = {}): Middleware {
|
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
function isSafeMethod(method: string, safeMethods: readonly RequestMethod[]): boolean {
|
|
168
|
+
return isRequestMethod(method) && safeMethods.includes(method)
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
/**
|
|
160
172
|
* Gets the CSRF token from the session. Creates one if missing.
|
|
161
173
|
*
|
|
@@ -163,12 +175,12 @@ export function csrf(options: CsrfOptions = {}): Middleware {
|
|
|
163
175
|
* @param tokenKey Session key that stores the token
|
|
164
176
|
* @returns The active CSRF token
|
|
165
177
|
*/
|
|
166
|
-
export function getCsrfToken(context:
|
|
167
|
-
|
|
178
|
+
export function getCsrfToken(context: AnyRequestContext, tokenKey = '_csrf'): string {
|
|
179
|
+
let session = context.get(Session)
|
|
180
|
+
if (session == null) {
|
|
168
181
|
throw new Error('Session is not started. Use session() middleware before csrf().')
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
let session = context.get(Session)
|
|
172
184
|
let token = session.get(tokenKey)
|
|
173
185
|
if (typeof token === 'string' && token !== '') {
|
|
174
186
|
return token
|
|
@@ -195,7 +207,7 @@ function createCsrfToken(): string {
|
|
|
195
207
|
function getErrorResponse(
|
|
196
208
|
options: CsrfOptions,
|
|
197
209
|
reason: CsrfFailureReason,
|
|
198
|
-
context:
|
|
210
|
+
context: AnyRequestContext,
|
|
199
211
|
): Response | Promise<Response> {
|
|
200
212
|
if (options.onError) {
|
|
201
213
|
return options.onError(reason, context)
|
|
@@ -213,7 +225,7 @@ function getErrorResponse(
|
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
async function resolveSubmittedToken(
|
|
216
|
-
context:
|
|
228
|
+
context: AnyRequestContext,
|
|
217
229
|
valueResolver: CsrfTokenResolver | undefined,
|
|
218
230
|
fieldName: string,
|
|
219
231
|
headerNames: readonly string[],
|
|
@@ -240,7 +252,7 @@ async function resolveSubmittedToken(
|
|
|
240
252
|
}
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
let formValue = context.
|
|
255
|
+
let formValue = context.get(FormData)?.get(fieldName)
|
|
244
256
|
if (typeof formValue === 'string') {
|
|
245
257
|
let trimmedFormValue = formValue.trim()
|
|
246
258
|
if (trimmedFormValue !== '') {
|
|
@@ -258,7 +270,7 @@ async function resolveSubmittedToken(
|
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
async function validateRequestOrigin(
|
|
261
|
-
context:
|
|
273
|
+
context: AnyRequestContext,
|
|
262
274
|
configuredOrigin: CsrfOrigin | undefined,
|
|
263
275
|
allowMissingOrigin: boolean,
|
|
264
276
|
defaultOrigin: string,
|
|
@@ -298,7 +310,7 @@ async function validateRequestOrigin(
|
|
|
298
310
|
return false
|
|
299
311
|
}
|
|
300
312
|
|
|
301
|
-
function getRequestOrigin(context:
|
|
313
|
+
function getRequestOrigin(context: AnyRequestContext): string | null {
|
|
302
314
|
let origin = context.headers.get('Origin')
|
|
303
315
|
if (origin != null && origin.trim() !== '') {
|
|
304
316
|
return origin
|