Skip to content

Commit b53c555

Browse files
Merge commit from fork
Co-authored-by: Matthew McNeely <matthew.mcneely@gmail.com>
1 parent 36b3d75 commit b53c555

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
@@ -39,11 +39,11 @@ func extractName(ctx context.Context) (string, error) {
3939
}
4040

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

4444
name, err := extractName(ctx)
4545
if err != nil {
46-
return nil, err
46+
return nil, nil, err
4747
}
4848

4949
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
@@ -54,6 +54,7 @@ type AuthQueryRewritingCase struct {
5454
// Dgraph upsert query and mutations built from the GQL
5555
DGQuery string
5656
DGQuerySec string
57+
DGVars map[string]string
5758
DGMutations []*dgraphMutation
5859
DGMutationsSec []*dgraphMutation
5960

@@ -444,7 +445,7 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, b []b
444445
require.NoError(t, err)
445446
}
446447

447-
dgQuery, err := testRewriter.Rewrite(ctx, gqlQuery)
448+
dgQuery, dgVars, err := testRewriter.Rewrite(ctx, gqlQuery)
448449

449450
if tcase.Error != nil {
450451
require.NotNil(t, err)
@@ -453,9 +454,14 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, b []b
453454
} else {
454455
require.NoError(t, err)
455456
require.Equal(t, tcase.DGQuery, dgraph.AsString(dgQuery))
457+
if len(tcase.DGVars) == 0 {
458+
require.Empty(t, dgVars)
459+
} else {
460+
require.Equal(t, tcase.DGVars, dgVars)
461+
}
456462
}
457463
// Check for unused variables.
458-
_, err = dql.Parse(dql.Request{Str: dgraph.AsString(dgQuery)})
464+
_, err = dql.Parse(dql.Request{Str: dgraph.AsString(dgQuery), Variables: dgVars})
459465
require.NoError(t, err)
460466
})
461467
}

graphql/resolve/query.go

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

4242
// A QueryRewriter can build a Dgraph dql.GraphQuery from a GraphQL query,
43+
// along with any DQL variable bindings (e.g. for parameterized password
44+
// queries) that must be supplied to the executor.
4345
type QueryRewriter interface {
44-
Rewrite(ctx context.Context, q schema.Query) ([]*dql.GraphQuery, error)
46+
Rewrite(ctx context.Context, q schema.Query) ([]*dql.GraphQuery, map[string]string, error)
4547
}
4648

4749
// QueryResolverFunc is an adapter that allows to build a QueryResolver from
@@ -121,7 +123,7 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que
121123
}
122124
}
123125

124-
dgQuery, err := qr.queryRewriter.Rewrite(ctx, query)
126+
dgQuery, vars, err := qr.queryRewriter.Rewrite(ctx, query)
125127
if err != nil {
126128
return emptyResult(schema.GQLWrapf(err, "couldn't rewrite query %s",
127129
query.ResponseName()))
@@ -130,7 +132,8 @@ func (qr *queryResolver) rewriteAndExecute(ctx context.Context, query schema.Que
130132

131133
queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration)
132134
queryTimer.Start()
133-
resp, err := qr.executor.Execute(ctx, &dgoapi.Request{Query: qry, ReadOnly: true}, query)
135+
resp, err := qr.executor.Execute(ctx,
136+
&dgoapi.Request{Query: qry, Vars: vars, ReadOnly: true}, query)
134137
queryTimer.Stop()
135138

136139
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
@@ -110,14 +110,16 @@ func getAuthSelector(queryType schema.QueryType) func(t schema.Type) *schema.Rul
110110
return queryAuthSelector
111111
}
112112

113-
// Rewrite rewrites a GraphQL query into DQL.
113+
// Rewrite rewrites a GraphQL query into DQL. It also returns any DQL variable
114+
// bindings (e.g. parameterized password values) that must be passed to the
115+
// executor alongside the query string.
114116
func (qr *queryRewriter) Rewrite(
115117
ctx context.Context,
116-
gqlQuery schema.Query) ([]*dql.GraphQuery, error) {
118+
gqlQuery schema.Query) ([]*dql.GraphQuery, map[string]string, error) {
117119

118120
customClaims, err := gqlQuery.GetAuthMeta().ExtractCustomClaims(ctx)
119121
if err != nil {
120-
return nil, err
122+
return nil, nil, err
121123
}
122124

123125
authRw := &authRewriter{
@@ -143,29 +145,30 @@ func (qr *queryRewriter) Rewrite(
143145
// ATM, I'm not sure how to hook into the GraphQL validator to get that to happen
144146
xid, uid, err := gqlQuery.IDArgValue()
145147
if err != nil {
146-
return nil, err
148+
return nil, nil, err
147149
}
148150

149151
dgQuery := rewriteAsGet(gqlQuery, uid, xid, authRw)
150-
return dgQuery, nil
152+
return dgQuery, nil, nil
151153
case schema.SimilarByIdQuery:
152154
xid, uid, err := gqlQuery.IDArgValue()
153155
if err != nil {
154-
return nil, err
156+
return nil, nil, err
155157
}
156-
return rewriteAsSimilarByIdQuery(gqlQuery, uid, xid, authRw), nil
158+
return rewriteAsSimilarByIdQuery(gqlQuery, uid, xid, authRw), nil, nil
157159
case schema.SimilarByEmbeddingQuery:
158-
return rewriteAsSimilarByEmbeddingQuery(gqlQuery, authRw), nil
160+
return rewriteAsSimilarByEmbeddingQuery(gqlQuery, authRw), nil, nil
159161
case schema.FilterQuery:
160-
return rewriteAsQuery(gqlQuery, authRw), nil
162+
return rewriteAsQuery(gqlQuery, authRw), nil, nil
161163
case schema.PasswordQuery:
162164
return passwordQuery(gqlQuery, authRw)
163165
case schema.AggregateQuery:
164-
return aggregateQuery(gqlQuery, authRw), nil
166+
return aggregateQuery(gqlQuery, authRw), nil, nil
165167
case schema.EntitiesQuery:
166-
return entitiesQuery(gqlQuery, authRw)
168+
queries, err := entitiesQuery(gqlQuery, authRw)
169+
return queries, nil, err
167170
default:
168-
return nil, errors.Errorf("unimplemented query type %s", gqlQuery.QueryType())
171+
return nil, nil, errors.Errorf("unimplemented query type %s", gqlQuery.QueryType())
169172
}
170173
}
171174

@@ -340,17 +343,18 @@ func aggregateQuery(query schema.Query, authRw *authRewriter) []*dql.GraphQuery
340343
return append([]*dql.GraphQuery{finalMainQuery}, dgQuery...)
341344
}
342345

343-
func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, error) {
346+
func passwordQuery(m schema.Query, authRw *authRewriter) (
347+
[]*dql.GraphQuery, map[string]string, error) {
344348
xid, uid, err := m.IDArgValue()
345349
if err != nil {
346-
return nil, err
350+
return nil, nil, err
347351
}
348352

349353
dgQuery := rewriteAsGet(m, uid, xid, authRw)
350354

351355
// Handle empty dgQuery
352356
if strings.HasSuffix(dgQuery[0].Attr, "()") {
353-
return dgQuery, nil
357+
return dgQuery, nil, nil
354358
}
355359

356360
// mainQuery is the query with check<Type>Password as Attr.
@@ -362,15 +366,24 @@ func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, err
362366
predicate := queriedType.DgraphPredicate(name)
363367
password := m.ArgValue(name).(string)
364368

369+
// Parameterize the password as a DQL variable so the value is bound by
370+
// the executor rather than interpolated into the query string. This
371+
// prevents DQL injection via passwords containing characters such as
372+
// a double-quote.
373+
const pwdVar = "$pwd0"
374+
if mainQuery.Args == nil {
375+
mainQuery.Args = make(map[string]string)
376+
}
377+
mainQuery.Args[pwdVar] = "string"
378+
365379
// This adds the checkPwd function
366380
op := &dql.GraphQuery{
367381
Attr: "checkPwd",
368382
Func: mainQuery.Func,
369383
Filter: mainQuery.Filter,
370384
Children: []*dql.GraphQuery{{
371-
Var: "pwd",
372-
Attr: fmt.Sprintf(`checkpwd(%s, "%s")`, predicate,
373-
password),
385+
Var: "pwd",
386+
Attr: fmt.Sprintf(`checkpwd(%s, %s)`, predicate, pwdVar),
374387
}},
375388
}
376389

@@ -397,7 +410,7 @@ func passwordQuery(m schema.Query, authRw *authRewriter) ([]*dql.GraphQuery, err
397410

398411
mainQuery.Filter = ft
399412

400-
return append(dgQuery, op), nil
413+
return append(dgQuery, op), map[string]string{pwdVar: password}, nil
401414
}
402415

403416
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
@@ -42,6 +42,7 @@ type QueryRewritingCase struct {
4242
GQLQuery string
4343
GQLVariables string
4444
DGQuery string
45+
DGVars map[string]string
4546
}
4647

4748
func TestQueryRewriting(t *testing.T) {
@@ -70,9 +71,14 @@ func TestQueryRewriting(t *testing.T) {
7071
require.NoError(t, err)
7172
gqlQuery := test.GetQuery(t, op)
7273

73-
dgQuery, err := testRewriter.Rewrite(context.Background(), gqlQuery)
74+
dgQuery, dgVars, err := testRewriter.Rewrite(context.Background(), gqlQuery)
7475
require.NoError(t, err)
7576
require.Equal(t, tcase.DGQuery, dgraph.AsString(dgQuery))
77+
if len(tcase.DGVars) == 0 {
78+
require.Empty(t, dgVars)
79+
} else {
80+
require.Equal(t, tcase.DGVars, dgVars)
81+
}
7682
})
7783
}
7884
}

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)