Skip to content

Commit cee702c

Browse files
Merge commit from fork
Co-authored-by: Matthew McNeely <matthew.mcneely@gmail.com>
1 parent 92c1d8d commit cee702c

7 files changed

Lines changed: 102 additions & 40 deletions

File tree

graphql/admin/current_user.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ func extractName(ctx context.Context) (string, error) {
2828
}
2929

3030
func (gsr *currentUserResolver) Rewrite(ctx context.Context,
31-
gqlQuery schema.Query) ([]*dql.GraphQuery, error) {
31+
gqlQuery schema.Query) ([]*dql.GraphQuery, map[string]string, error) {
3232

3333
name, err := extractName(ctx)
3434
if err != nil {
35-
return nil, err
35+
return nil, nil, err
3636
}
3737

3838
gqlQuery.Rename("getUser")

graphql/resolve/auth_query_test.yaml

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,15 +1760,17 @@
17601760
}
17611761
}
17621762
dgquery: |-
1763-
query {
1763+
query checkPwd($pwd0: string) {
17641764
checkUserPassword(func: eq(User.username, "user")) @filter((eq(val(pwd), 1) AND type(User))) {
17651765
User.username : User.username
17661766
dgraph.uid : uid
17671767
}
17681768
checkPwd(func: eq(User.username, "user")) @filter(type(User)) {
1769-
pwd as checkpwd(User.password, "Password")
1769+
pwd as checkpwd(User.password, $pwd0)
17701770
}
17711771
}
1772+
dgvars:
1773+
$pwd0: Password
17721774

17731775
- name: Password Query with RBAC rule true
17741776
gqlquery: |
@@ -1782,7 +1784,7 @@
17821784
jwtvar:
17831785
ROLE: Admin
17841786
dgquery: |-
1785-
query {
1787+
query checkPwd($pwd0: string) {
17861788
checkLogPassword(func: uid(LogRoot)) @filter((eq(val(pwd), 1) AND type(Log))) {
17871789
Log.id : uid
17881790
Log.logs : Log.logs
@@ -1791,9 +1793,11 @@
17911793
LogRoot as var(func: uid(Log_1))
17921794
Log_1 as var(func: uid(0x123))
17931795
checkPwd(func: uid(LogRoot)) @filter(type(Log)) {
1794-
pwd as checkpwd(Log.pwd, "something")
1796+
pwd as checkpwd(Log.pwd, $pwd0)
17951797
}
17961798
}
1799+
dgvars:
1800+
$pwd0: something
17971801

17981802
- name: Password Query with RBAC rule false
17991803
gqlquery: |
@@ -1825,7 +1829,7 @@
18251829
jwtvar:
18261830
USER: user1
18271831
dgquery: |-
1828-
query {
1832+
query checkPwd($pwd0: string) {
18291833
checkProjectPassword(func: uid(ProjectRoot)) @filter((eq(val(pwd), 1) AND type(Project))) {
18301834
Project.name : Project.name
18311835
Project.projID : uid
@@ -1853,9 +1857,11 @@
18531857
}
18541858
}
18551859
checkPwd(func: uid(ProjectRoot)) @filter(type(Project)) {
1856-
pwd as checkpwd(Project.pwd, "something")
1860+
pwd as checkpwd(Project.pwd, $pwd0)
18571861
}
18581862
}
1863+
dgvars:
1864+
$pwd0: something
18591865

18601866
- name:
18611867
Type with password query should apply Interface's password rules and along with its own auth
@@ -1872,7 +1878,7 @@
18721878
ANS: "true"
18731879
USER: ADMIN
18741880
dgquery: |-
1875-
query {
1881+
query checkPwd($pwd0: string) {
18761882
checkQuestionPassword(func: uid(QuestionRoot)) @filter((eq(val(pwd), 1) AND type(Question))) {
18771883
Question.id : uid
18781884
Question.text : Post.text
@@ -1884,9 +1890,11 @@
18841890
Question.text : Post.text
18851891
}
18861892
checkPwd(func: uid(QuestionRoot)) @filter(type(Question)) {
1887-
pwd as checkpwd(Question.pwd, "something")
1893+
pwd as checkpwd(Question.pwd, $pwd0)
18881894
}
18891895
}
1896+
dgvars:
1897+
$pwd0: something
18901898

18911899
- name:
18921900
Type which inherits password auth rules from interfaces returns no results when auth rules fail
@@ -1918,7 +1926,7 @@
19181926
ANS: "true"
19191927
USER: ADMIN
19201928
dgquery: |-
1921-
query {
1929+
query checkPwd($pwd0: string) {
19221930
checkPostPassword(func: uid(PostRoot)) @filter((eq(val(pwd), 1) AND type(Post))) {
19231931
dgraph.type
19241932
Post.text : Post.text
@@ -1939,9 +1947,11 @@
19391947
}
19401948
Answer_6 as var(func: type(Answer))
19411949
checkPwd(func: uid(PostRoot)) @filter(type(Post)) {
1942-
pwd as checkpwd(Post.pwd, "something")
1950+
pwd as checkpwd(Post.pwd, $pwd0)
19431951
}
19441952
}
1953+
dgvars:
1954+
$pwd0: something
19451955

19461956
- name: Entities query with query auth rules
19471957
gqlquery: |

graphql/resolve/auth_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type AuthQueryRewritingCase struct {
4343
// Dgraph upsert query and mutations built from the GQL
4444
DGQuery string
4545
DGQuerySec string
46+
DGVars map[string]string
4647
DGMutations []*dgraphMutation
4748
DGMutationsSec []*dgraphMutation
4849

@@ -433,7 +434,7 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, b []b
433434
require.NoError(t, err)
434435
}
435436

436-
dgQuery, err := testRewriter.Rewrite(ctx, gqlQuery)
437+
dgQuery, dgVars, err := testRewriter.Rewrite(ctx, gqlQuery)
437438

438439
if tcase.Error != nil {
439440
require.NotNil(t, err)
@@ -442,9 +443,14 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, b []b
442443
} else {
443444
require.NoError(t, err)
444445
require.Equal(t, tcase.DGQuery, dgraph.AsString(dgQuery))
446+
if len(tcase.DGVars) == 0 {
447+
require.Empty(t, dgVars)
448+
} else {
449+
require.Equal(t, tcase.DGVars, dgVars)
450+
}
445451
}
446452
// Check for unused variables.
447-
_, err = dql.Parse(dql.Request{Str: dgraph.AsString(dgQuery)})
453+
_, err = dql.Parse(dql.Request{Str: dgraph.AsString(dgQuery), Variables: dgVars})
448454
require.NoError(t, err)
449455
})
450456
}

graphql/resolve/query.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ type QueryResolver interface {
2929
}
3030

3131
// A QueryRewriter can build a Dgraph dql.GraphQuery from a GraphQL query,
32+
// along with any DQL variable bindings (e.g. for parameterized password
33+
// queries) that must be supplied to the executor.
3234
type QueryRewriter interface {
33-
Rewrite(ctx context.Context, q schema.Query) ([]*dql.GraphQuery, error)
35+
Rewrite(ctx context.Context, q schema.Query) ([]*dql.GraphQuery, map[string]string, error)
3436
}
3537

3638
// QueryResolverFunc is an adapter that allows to build a QueryResolver from
@@ -110,7 +112,7 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que
110112
}
111113
}
112114

113-
dgQuery, err := qr.queryRewriter.Rewrite(ctx, query)
115+
dgQuery, vars, err := qr.queryRewriter.Rewrite(ctx, query)
114116
if err != nil {
115117
return emptyResult(schema.GQLWrapf(err, "couldn't rewrite query %s",
116118
query.ResponseName()))
@@ -119,7 +121,8 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que
119121

120122
queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration)
121123
queryTimer.Start()
122-
resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, ReadOnly: true}, query)
124+
resp, err := qr.executor.Execute(ctx,
125+
&dgoapi.Request{Query: qry, Vars: vars, ReadOnly: true}, query)
123126
queryTimer.Stop()
124127

125128
if err != nil && !x.IsGqlErrorList(err) {

graphql/resolve/query_rewriter.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,16 @@ func getAuthSelector(queryType schema.QueryType) func(t schema.Type) *schema.Rul
9999
return queryAuthSelector
100100
}
101101

102-
// Rewrite rewrites a GraphQL query into DQL.
102+
// Rewrite rewrites a GraphQL query into DQL. It also returns any DQL variable
103+
// bindings (e.g. parameterized password values) that must be passed to the
104+
// executor alongside the query string.
103105
func (qr *queryRewriter) Rewrite(
104106
ctx context.Context,
105-
gqlQuery schema.Query) ([]*dql.GraphQuery, error) {
107+
gqlQuery schema.Query) ([]*dql.GraphQuery, map[string]string, error) {
106108

107109
customClaims, err := gqlQuery.GetAuthMeta().ExtractCustomClaims(ctx)
108110
if err != nil {
109-
return nil, err
111+
return nil, nil, err
110112
}
111113

112114
authRw := &authRewriter{
@@ -132,29 +134,30 @@ func (qr *queryRewriter) Rewrite(
132134
// ATM, I'm not sure how to hook into the GraphQL validator to get that to happen
133135
xid, uid, err := gqlQuery.IDArgValue()
134136
if err != nil {
135-
return nil, err
137+
return nil, nil, err
136138
}
137139

138140
dgQuery := rewriteAsGet(gqlQuery, uid, xid, authRw)
139-
return dgQuery, nil
141+
return dgQuery, nil, nil
140142
case schema.SimilarByIdQuery:
141143
xid, uid, err := gqlQuery.IDArgValue()
142144
if err != nil {
143-
return nil, err
145+
return nil, nil, err
144146
}
145-
return rewriteAsSimilarByIdQuery(gqlQuery, uid, xid, authRw), nil
147+
return rewriteAsSimilarByIdQuery(gqlQuery, uid, xid, authRw), nil, nil
146148
case schema.SimilarByEmbeddingQuery:
147-
return rewriteAsSimilarByEmbeddingQuery(gqlQuery, authRw), nil
149+
return rewriteAsSimilarByEmbeddingQuery(gqlQuery, authRw), nil, nil
148150
case schema.FilterQuery:
149-
return rewriteAsQuery(gqlQuery, authRw), nil
151+
return rewriteAsQuery(gqlQuery, authRw), nil, nil
150152
case schema.PasswordQuery:
151153
return passwordQuery(gqlQuery, authRw)
152154
case schema.AggregateQuery:
153-
return aggregateQuery(gqlQuery, authRw), nil
155+
return aggregateQuery(gqlQuery, authRw), nil, nil
154156
case schema.EntitiesQuery:
155-
return entitiesQuery(gqlQuery, authRw)
157+
queries, err := entitiesQuery(gqlQuery, authRw)
158+
return queries, nil, err
156159
default:
157-
return nil, errors.Errorf("unimplemented query type %s", gqlQuery.QueryType())
160+
return nil, nil, errors.Errorf("unimplemented query type %s", gqlQuery.QueryType())
158161
}
159162
}
160163

@@ -329,17 +332,18 @@ func aggregateQuery(query schema.Query, authRw *authRewriter) []*dql.GraphQuery
329332
return append([]*dql.GraphQuery{finalMainQuery}, dgQuery...)
330333
}
331334

332-
func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, error) {
335+
func passwordQuery(m schema.Query, authRw *authRewriter) (
336+
[]*dql.GraphQuery, map[string]string, error) {
333337
xid, uid, err := m.IDArgValue()
334338
if err != nil {
335-
return nil, err
339+
return nil, nil, err
336340
}
337341

338342
dgQuery := rewriteAsGet(m, uid, xid, authRw)
339343

340344
// Handle empty dgQuery
341345
if strings.HasSuffix(dgQuery[0].Attr, "()") {
342-
return dgQuery, nil
346+
return dgQuery, nil, nil
343347
}
344348

345349
// mainQuery is the query with check<Type>Password as Attr.
@@ -351,15 +355,24 @@ func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, err
351355
predicate := queriedType.DgraphPredicate(name)
352356
password := m.ArgValue(name).(string)
353357

358+
// Parameterize the password as a DQL variable so the value is bound by
359+
// the executor rather than interpolated into the query string. This
360+
// prevents DQL injection via passwords containing characters such as
361+
// a double-quote.
362+
const pwdVar = "$pwd0"
363+
if mainQuery.Args == nil {
364+
mainQuery.Args = make(map[string]string)
365+
}
366+
mainQuery.Args[pwdVar] = "string"
367+
354368
// This adds the checkPwd function
355369
op := &dql.GraphQuery{
356370
Attr: "checkPwd",
357371
Func: mainQuery.Func,
358372
Filter: mainQuery.Filter,
359373
Children: []*dql.GraphQuery{{
360-
Var: "pwd",
361-
Attr: fmt.Sprintf(`checkpwd(%s, "%s")`, predicate,
362-
password),
374+
Var: "pwd",
375+
Attr: fmt.Sprintf(`checkpwd(%s, %s)`, predicate, pwdVar),
363376
}},
364377
}
365378

@@ -386,7 +399,7 @@ func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, err
386399

387400
mainQuery.Filter = ft
388401

389-
return append(dgQuery, op), nil
402+
return append(dgQuery, op), map[string]string{pwdVar: password}, nil
390403
}
391404

392405
func intersection(a, b []uint64) []uint64 {

graphql/resolve/query_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type QueryRewritingCase struct {
3131
GQLQuery string
3232
GQLVariables string
3333
DGQuery string
34+
DGVars map[string]string
3435
}
3536

3637
func TestQueryRewriting(t *testing.T) {
@@ -59,9 +60,14 @@ func TestQueryRewriting(t *testing.T) {
5960
require.NoError(t, err)
6061
gqlQuery := test.GetQuery(t, op)
6162

62-
dgQuery, err := testRewriter.Rewrite(context.Background(), gqlQuery)
63+
dgQuery, dgVars, err := testRewriter.Rewrite(context.Background(), gqlQuery)
6364
require.NoError(t, err)
6465
require.Equal(t, tcase.DGQuery, dgraph.AsString(dgQuery))
66+
if len(tcase.DGVars) == 0 {
67+
require.Empty(t, dgVars)
68+
} else {
69+
require.Equal(t, tcase.DGVars, dgVars)
70+
}
6571
})
6672
}
6773
}

graphql/resolve/query_test.yaml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2446,15 +2446,17 @@
24462446
}
24472447
}
24482448
dgquery: |-
2449-
query {
2449+
query checkPwd($pwd0: string) {
24502450
checkUserPassword(func: eq(User.name, "user1")) @filter((eq(val(pwd), 1) AND type(User))) {
24512451
User.name : User.name
24522452
dgraph.uid : uid
24532453
}
24542454
checkPwd(func: eq(User.name, "user1")) @filter(type(User)) {
2455-
pwd as checkpwd(User.pwd, "Password")
2455+
pwd as checkpwd(User.pwd, $pwd0)
24562456
}
24572457
}
2458+
dgvars:
2459+
$pwd0: Password
24582460

24592461
- name: Password query with alias
24602462
gqlquery: |
@@ -2464,15 +2466,37 @@
24642466
}
24652467
}
24662468
dgquery: |-
2469+
query checkPwd($pwd0: string) {
2470+
checkUserPassword(func: eq(User.name, "user1")) @filter((eq(val(pwd), 1) AND type(User))) {
2471+
User.name : User.name
2472+
dgraph.uid : uid
2473+
}
2474+
checkPwd(func: eq(User.name, "user1")) @filter(type(User)) {
2475+
pwd as checkpwd(User.pwd, $pwd0)
2476+
}
2477+
}
2478+
dgvars:
2479+
$pwd0: Password
2480+
2481+
- name: Password query parameterizes pwd to prevent DQL injection
2482+
gqlquery: |
24672483
query {
2484+
checkUserPassword(name: "user1", pwd: "evil\") { msg } injected(func: has(User.name)) #") {
2485+
name
2486+
}
2487+
}
2488+
dgquery: |-
2489+
query checkPwd($pwd0: string) {
24682490
checkUserPassword(func: eq(User.name, "user1")) @filter((eq(val(pwd), 1) AND type(User))) {
24692491
User.name : User.name
24702492
dgraph.uid : uid
24712493
}
24722494
checkPwd(func: eq(User.name, "user1")) @filter(type(User)) {
2473-
pwd as checkpwd(User.pwd, "Password")
2495+
pwd as checkpwd(User.pwd, $pwd0)
24742496
}
24752497
}
2498+
dgvars:
2499+
$pwd0: 'evil") { msg } injected(func: has(User.name)) #'
24762500

24772501
- name: Rewrite without custom fields
24782502
gqlquery: |

0 commit comments

Comments
 (0)