Skip to content

Commit bc18622

Browse files
committed
fix: Security improvements, typing fixes
Fixes potential security hole in no_cache option.
1 parent 17911d5 commit bc18622

File tree

16 files changed

+267
-161
lines changed

16 files changed

+267
-161
lines changed

README.md

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,25 @@ function getUser(req) {
103103

104104
// Rules
105105

106-
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
107-
return ctx.user !== null
108-
})
106+
/* Read more about cache options down in the `rules/cache` section. */
109107

110-
const isAdmin = rule()(async (parent, args, ctx, info) => {
111-
return ctx.user.role === 'admin'
112-
})
108+
const isAuthenticated = rule({ cache: 'contextual' })(
109+
async (parent, args, ctx, info) => {
110+
return ctx.user !== null
111+
},
112+
)
113113

114-
const isEditor = rule()(async (parent, args, ctx, info) => {
115-
return ctx.user.role === 'editor'
116-
})
114+
const isAdmin = rule({ cache: 'contextual' })(
115+
async (parent, args, ctx, info) => {
116+
return ctx.user.role === 'admin'
117+
},
118+
)
119+
120+
const isEditor = rule({ cache: 'contextual' })(
121+
async (parent, args, ctx, info) => {
122+
return ctx.user.role === 'editor'
123+
},
124+
)
117125

118126
// Permissions
119127

@@ -274,25 +282,34 @@ const admin = bool =>
274282
)
275283
```
276284

277-
- Cache is enabled by default across all rules. To prevent `cache` generation, set `{ cache: 'no_cache' }` or `{ cache: false }` when generating a rule.
278-
- By default, no rule is executed more than once in complete query execution. This accounts for significantly better load times and quick responses.
285+
- Cache is disabled by default. To enable `cache` generation, set cache option when generating a rule.
279286

280287
##### Cache
281288

282289
You can choose from three different cache options.
283290

284291
1. `no_cache` - prevents rules from being cached.
285-
1. `contextual` - use when rule only relies on `ctx` parameter.
286-
1. `strict` - use when rule relies on `parent` or `args` parameter as well.
292+
1. `contextual` - use when rule only relies on `context` parameter (useful for authentication).
293+
1. `strict` - use when rule relies on `parent` or `args` parameter as well (field specific modifications).
287294

288295
```ts
289296
// Contextual
290-
const admin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
291-
return ctx.user.isAdmin
292-
})
297+
const isAdmin = rule({ cache: 'contextual' })(
298+
async (parent, args, ctx, info) => {
299+
return ctx.user.isAdmin
300+
},
301+
)
293302
294303
// Strict
295-
const admin = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
304+
const canSeeUserSensitiveData = rule({ cache: 'strict' })(
305+
async (parent, args, ctx, info) => {
306+
/* The id of observed User matches the id of authenticated viewer. */
307+
return ctx.viewer.id === parent.id
308+
},
309+
)
310+
311+
// No-cache (defuault)
312+
const admin = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => {
296313
return ctx.user.isAdmin || args.code === 'secret' || parent.id === 'theone'
297314
})
298315
```
@@ -629,7 +646,7 @@ See [#126](https://github.com/maticzav/graphql-shield/issues/126#issuecomment-41
629646

630647
#### A rule is executed only once even though the dataset contains multiple values (and thus should execute the rule multiple times)
631648

632-
This occurs because of caching. When the cache is set to "contextual" only the contextual variable of the rule is expected to be evaluated. Setting the cache to "strict" allows the rule to rely on parent and args parameters as well.
649+
This occurs because of caching. When the cache is set to `contextual` only the contextual variable of the rule is expected to be evaluated. Setting the cache to `strict` allows the rule to rely on parent and args parameters as well, while setting the cache to `no_cache` won't cache result at all.
633650

634651
## Contributors
635652

examples/advanced/src/permissions/rules.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import { Context, getUserEmail } from '../utils'
66

77
// To see the effect with no cache, set { cache: false } in isCustomer rule.
88

9-
export const isGrocer = rule()(async (parent, args, ctx: Context, info) => {
10-
// console.log('SHIELD: IsGrocer?')
9+
export const isGrocer = rule({ cache: 'contextual' })(
10+
async (parent, args, ctx: Context, info) => {
11+
// console.log('SHIELD: IsGrocer?')
1112

12-
const email = getUserEmail(ctx)
13-
return ctx.db.exists.Grocer({ email })
14-
})
13+
const email = getUserEmail(ctx)
14+
return ctx.db.exists.Grocer({ email })
15+
},
16+
)
1517

16-
export const isCustomer = rule({ cache: true })(
18+
export const isCustomer = rule({ cache: 'contextual' })(
1719
async (parent, args, ctx: Context, info) => {
1820
// console.log('SHIELD: IsCustomer?')
1921

examples/basic/index.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,23 @@ function getUser(req) {
7575

7676
// Rules
7777

78-
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
79-
return ctx.user !== null
80-
})
78+
const isAuthenticated = rule({ cache: 'contextual' })(
79+
async (parent, args, ctx, info) => {
80+
return ctx.user !== null
81+
},
82+
)
8183

82-
const isAdmin = rule()(async (parent, args, ctx, info) => {
83-
return ctx.user.role === 'admin'
84-
})
84+
const isAdmin = rule({ cache: 'contextual' })(
85+
async (parent, args, ctx, info) => {
86+
return ctx.user.role === 'admin'
87+
},
88+
)
8589

86-
const isEditor = rule()(async (parent, args, ctx, info) => {
87-
return ctx.user.role === 'editor'
88-
})
90+
const isEditor = rule({ cache: 'contextual' })(
91+
async (parent, args, ctx, info) => {
92+
return ctx.user.role === 'editor'
93+
},
94+
)
8995

9096
// Permissions
9197

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
const {rule} = require('graphql-shield');
1+
const { rule } = require('graphql-shield')
22

3-
const isAuthenticated = rule()((parent, args, ctx) => {
4-
return Boolean(ctx.request.userId);
5-
});
3+
const isAuthenticated = rule({ cache: 'contextual' })((parent, args, ctx) => {
4+
return Boolean(ctx.request.userId)
5+
})
66

77
module.exports = {
8-
isAuthenticated
9-
};
8+
isAuthenticated,
9+
}

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ module.exports = {
1717
verbose: true,
1818
coverageDirectory: './coverage',
1919
coverageReporters: ['json', 'lcov', 'text', 'clover', 'html'],
20+
globals: {
21+
'ts-jest': {
22+
tsConfig: 'tsconfig.test.json',
23+
},
24+
},
2025
}

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
"author": "Matic Zavadlal <[email protected]>",
88
"scripts": {
99
"compile": "tsc -d",
10-
"coverage": "yarn codecov",
10+
"coverage": "codecov",
1111
"clean": "rimraf dist",
1212
"docz:dev": "docz dev",
1313
"docz:build": "docz build",
1414
"lint": "tslint --project tsconfig.json {src}/**/*.ts && prettier-check --ignore-path .gitignore src/**/*.ts",
1515
"postinstall": "lightercollective postinstall",
1616
"posttest": "npm-run-all lint",
1717
"prepublishOnly": "npm-run-all compile",
18-
"prerelease": "npm-run-all test",
19-
"pretest": "npm-run-all clean compile",
2018
"release": "semantic-release",
21-
"test": "NODE_ENV=test jest"
19+
"test": "npm-run-all --parallel test:*",
20+
"test:jest": "NODE_ENV=test jest",
21+
"test:types": "tsc --noEmit"
2222
},
2323
"dependencies": {
2424
"@types/yup": "0.26.23",
@@ -38,17 +38,17 @@
3838
"docz": "0.13.7",
3939
"docz-theme-default": "0.13.7",
4040
"graphql": "14.3.1",
41-
"graphql-middleware": "3.0.2",
41+
"graphql-middleware": "4.0.1",
4242
"graphql-tools": "4.0.5",
43-
"graphql-yoga": "1.18.1",
43+
"graphql-yoga": "1.18.3",
4444
"husky": "3.0.4",
4545
"jest": "24.9.0",
4646
"npm-run-all": "4.1.5",
4747
"prettier": "1.18.2",
4848
"prettier-check": "2.0.0",
4949
"pretty-quick": "1.11.1",
5050
"remark-emoji": "2.0.2",
51-
"rimraf": "2.7.1",
51+
"rimraf": "3.0.0",
5252
"semantic-release": "15.13.24",
5353
"ts-jest": "24.0.2",
5454
"ts-node": "8.3.0",
@@ -59,7 +59,7 @@
5959
},
6060
"peerDependencies": {
6161
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0",
62-
"graphql-middleware": "^2.0.0 || ^3.0.0"
62+
"graphql-middleware": "^2.0.0 || ^3.0.0 || ^4.0.0"
6363
},
6464
"resolutions": {
6565
"graphql-yoga/graphql": "14.3.1"

src/constructors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const inputRule = <T>(
7070
schema?: (yup: typeof Yup) => Yup.Schema<T>,
7171
) => {
7272
if (typeof name === 'string') {
73-
return new InputRule(name, schema(Yup))
73+
return new InputRule(name, schema!(Yup))
7474
} else {
7575
return new InputRule(Math.random().toString(), name(Yup))
7676
}

src/generator.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
withDefault,
2626
} from './utils'
2727
import { ValidationError } from './validation'
28+
import { IMiddlewareWithOptions } from 'graphql-middleware/dist/types'
2829

2930
/**
3031
*
@@ -37,9 +38,14 @@ import { ValidationError } from './validation'
3738
function generateFieldMiddlewareFromRule(
3839
rule: ShieldRule,
3940
options: IOptions,
40-
): IMiddlewareFunction {
41+
): IMiddlewareFunction<object, object, IShieldContext> {
4142
async function middleware(
42-
resolve: (parent, args, ctx, info) => any,
43+
resolve: (
44+
parent: object,
45+
args: object,
46+
ctx: IShieldContext,
47+
info: GraphQLResolveInfo,
48+
) => Promise<any>,
4349
parent: { [key: string]: any },
4450
args: { [key: string]: any },
4551
ctx: IShieldContext,
@@ -53,7 +59,6 @@ function generateFieldMiddlewareFromRule(
5359
if (!ctx._shield) {
5460
ctx._shield = {
5561
cache: {},
56-
hashFunction: options.hashFunction,
5762
}
5863
}
5964

@@ -83,17 +88,17 @@ function generateFieldMiddlewareFromRule(
8388
return {
8489
fragment: rule.extractFragment(),
8590
resolve: middleware,
86-
}
91+
} as IMiddlewareWithOptions<object, object, IShieldContext>
8792
}
8893

8994
if (isLogicRule(rule)) {
9095
return {
9196
fragments: rule.extractFragments(),
9297
resolve: middleware,
93-
}
98+
} as IMiddlewareWithOptions<object, object, IShieldContext>
9499
}
95100

96-
return middleware
101+
return middleware as IMiddlewareFunction<object, object, IShieldContext>
97102
}
98103

99104
/**

0 commit comments

Comments
 (0)