Skip to content

Commit

Permalink
Merge pull request #14216 from hmac/hmac-graphql-enum
Browse files Browse the repository at this point in the history
Ruby: Restrict GraphQL remote flow sources
  • Loading branch information
hmac authored Oct 13, 2023
2 parents 5e92178 + 2214cae commit 1297acf
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 30 deletions.
4 changes: 4 additions & 0 deletions ruby/ql/lib/change-notes/2023-09-18-graphql-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* GraphQL enums are no longer considered remote flow sources.
206 changes: 189 additions & 17 deletions ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,46 @@ class GraphqlFieldDefinitionMethodCall extends GraphqlSchemaObjectClassMethodCal

/** Gets the name of this GraphQL field. */
string getFieldName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }

/**
* Gets the type of this field.
*/
GraphqlType getFieldType() { result = this.getArgument(1) }

/**
* Gets an argument call inside this field definition.
*/
GraphqlFieldArgumentDefinitionMethodCall getAnArgumentCall() { result = this.getArgumentCall(_) }

/**
* Gets the argument call for `name` inside this field definition.
*/
GraphqlFieldArgumentDefinitionMethodCall getArgumentCall(string name) {
result.getEnclosingCallable() = this.getBlock() and result.getArgumentName() = name
}
}

/**
* A call to `argument` in a GraphQL InputObject class.
*/
class GraphqlInputObjectArgumentDefinitionCall extends DataFlow::CallNode {
GraphqlInputObjectArgumentDefinitionCall() {
this =
graphQlSchema()
.getMember("InputObject")
.getADescendentModule()
.getAnOwnModuleSelf()
.getAMethodCall("argument")
}

/** Gets the name of the argument (i.e. the first argument to this `argument` method call) */
string getArgumentName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }

/** Gets the type of this argument */
GraphqlType getArgumentType() { result = this.getArgument(1).asExpr().getExpr() }

/** Gets the class representing the receiver of this method. */
ClassDeclaration getReceiverClass() { result = this.asExpr().getExpr().getEnclosingModule() }
}

/**
Expand Down Expand Up @@ -289,6 +329,64 @@ private class GraphqlFieldArgumentDefinitionMethodCall extends GraphqlSchemaObje

/** Gets the name of the argument (i.e. the first argument to this `argument` method call) */
string getArgumentName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }

/** Gets the type of this argument */
GraphqlType getArgumentType() { result = this.getArgument(1) }

/**
* Gets the element type of this argument, if it is an array.
* For example if the argument type is `[String]`, this predicate yields `String`.
*/
GraphqlType getArgumentElementType() {
result =
any(ArrayLiteral lit | lit = this.getArgument(1) and lit.getNumberOfElements() = 1)
.getElement(0)
}
}

private class GraphqlType extends ConstantAccess {
/**
* Gets the module corresponding to this type, if it exists.
*/
Module getModule() { result.getAnImmediateReference() = this }

/**
* Gets the type of a field/argument of this type, if it is an object type.
*/
GraphqlType getAFieldOrArgument() { result = this.getFieldOrArgument(_) }

/**
* Gets the type of the `name` field/argument of this type, if it exists.
*/
GraphqlType getFieldOrArgument(string name) {
result =
any(GraphqlFieldDefinitionMethodCall field |
field.getFieldName() = name and
this.getModule().getADeclaration() = field.getReceiverClass()
).getFieldType() or
result =
any(GraphqlInputObjectArgumentDefinitionCall arg |
arg.getArgumentName() = name and this.getModule().getADeclaration() = arg.getReceiverClass()
).getArgumentType()
}

/**
* Holds if this type is an enum.
*/
predicate isEnum() {
API::getTopLevelMember("GraphQL")
.getMember("Schema")
.getMember("Enum")
.getADescendentModule()
.getAnImmediateReference()
.asExpr()
.getExpr() = this
}

/**
* Holds if this type is scalar - i.e. it is neither an object or an enum.
*/
predicate isScalar() { not exists(this.getAFieldOrArgument()) and not this.isEnum() }
}

/**
Expand Down Expand Up @@ -350,29 +448,26 @@ class GraphqlFieldResolutionMethod extends Method, Http::Server::RequestHandler:

/** Gets the method call which is the definition of the field corresponding to this resolver method. */
GraphqlFieldDefinitionMethodCall getDefinition() {
result
.getKeywordArgument("resolver_method")
.getConstantValue()
.isStringlikeValue(this.getName())
or
not exists(result.getKeywordArgument("resolver_method").(SymbolLiteral)) and
result.getFieldName() = this.getName()
result.getEnclosingModule() = this.getEnclosingModule() and
(
result
.getKeywordArgument("resolver_method")
.getConstantValue()
.isStringlikeValue(this.getName())
or
not exists(result.getKeywordArgument("resolver_method").(SymbolLiteral)) and
result.getFieldName() = this.getName()
)
}

// check for a named argument the same name as a defined argument for this field
override Parameter getARoutedParameter() {
result = this.getAParameter() and
exists(GraphqlFieldArgumentDefinitionMethodCall argDefn |
argDefn.getEnclosingCallable() = this.getDefinition().getBlock() and
(
result.(KeywordParameter).hasName(argDefn.getArgumentName())
or
// TODO this will cause false positives because now *anything* in the **args
// param will be flagged as RoutedParameter/RemoteFlowSource, but really
// only the hash keys corresponding to the defined arguments are user input
// others could be things defined in the `:extras` keyword argument to the `argument`
result instanceof HashSplatParameter // often you see `def field(**args)`
)
argDefn = this.getDefinition().getAnArgumentCall() and
[argDefn.getArgumentType(), argDefn.getArgumentElementType()].isScalar()
|
result.(KeywordParameter).hasName(argDefn.getArgumentName())
)
}

Expand All @@ -383,3 +478,80 @@ class GraphqlFieldResolutionMethod extends Method, Http::Server::RequestHandler:
/** Gets the class containing this method. */
GraphqlSchemaObjectClass getGraphqlClass() { result = schemaObjectClass }
}

private DataFlow::CallNode hashParameterAccess(
GraphqlFieldResolutionMethod method, HashSplatParameter param, GraphqlType type
) {
exists(
DataFlow::LocalSourceNode paramNode, GraphqlFieldArgumentDefinitionMethodCall def, string key
|
param = method.getAParameter() and
paramNode.(DataFlow::ParameterNode).getParameter() = param and
def = method.getDefinition().getAnArgumentCall() and
(
// Direct access to the params hash
[def.getArgumentType(), def.getArgumentElementType()] = type and
def.getArgumentName() = key and
paramNode.flowsTo(hashAccess(result, key))
or
// Nested access
exists(GraphqlType type2 |
hashParameterAccess(method, param, type2)
.(DataFlow::LocalSourceNode)
.flowsTo(hashAccess(result, key)) and
type2.getFieldOrArgument(key) = type
)
)
)
}

private DataFlow::Node parameterAccess(
GraphqlFieldResolutionMethod method, DataFlow::LocalSourceNode param, GraphqlType type
) {
param = getAGraphqlParameter(method, type) and
result = param
or
exists(string key, GraphqlType type2 |
param = parameterAccess(method, _, type2) and
param.flowsTo(hashAccess(result, key)) and
type2.getFieldOrArgument(key) = type
)
}

private DataFlow::ParameterNode getAGraphqlParameter(
GraphqlFieldResolutionMethod method, GraphqlType type
) {
result.getCallable() = method and
(
result.getParameter() instanceof KeywordParameter and
exists(GraphqlFieldArgumentDefinitionMethodCall c |
c = method.getDefinition().getArgumentCall(result.getName())
|
type = [c.getArgumentType(), c.getArgumentElementType()]
)
or
result.getParameter() instanceof SimpleParameter and
type = method.getDefinition().getFieldType()
)
}

/**
* Gets the receiver of the hash access `access` with key `key`.
* For example, in `h["foo"]` the receiver is `h`, the key is "foo"
* and `access` is the dataflow node for the whole expression.
*/
private DataFlow::Node hashAccess(DataFlow::CallNode access, string key) {
access.asExpr() instanceof ExprNodes::ElementReferenceCfgNode and
access.getArgument(0).getConstantValue().isStringlikeValue(key) and
access.getReceiver() = result
}

private class GraphqlParameterAccess extends RemoteFlowSource::Range {
GraphqlParameterAccess() {
exists(GraphqlType type | type.isScalar() |
this = hashParameterAccess(_, _, type) or this = parameterAccess(_, _, type)
)
}

override string getSourceType() { result = "GraphQL" }
}
57 changes: 49 additions & 8 deletions ruby/ql/test/library-tests/frameworks/graphql/GraphQL.expected
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
graphqlSchemaObjectClass
| app/graphql/types/base_object.rb:2:3:4:5 | BaseObject |
| app/graphql/types/mutation_type.rb:2:3:4:5 | MutationType |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType |
| app/graphql/types/post.rb:1:1:6:5 | Post |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType |
graphqlSchemaObjectFieldDefinition
| app/graphql/types/mutation_type.rb:2:3:4:5 | MutationType | app/graphql/types/mutation_type.rb:3:5:3:44 | call to field |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType | app/graphql/types/query_type.rb:3:5:5:40 | call to field |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType | app/graphql/types/query_type.rb:7:5:9:7 | call to field |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType | app/graphql/types/query_type.rb:15:5:17:7 | call to field |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType | app/graphql/types/query_type.rb:24:5:26:7 | call to field |
| app/graphql/types/query_type.rb:2:3:45:5 | QueryType | app/graphql/types/query_type.rb:32:5:35:7 | call to field |
| app/graphql/types/post.rb:1:1:6:5 | Post | app/graphql/types/post.rb:2:5:2:24 | call to field |
| app/graphql/types/post.rb:1:1:6:5 | Post | app/graphql/types/post.rb:3:5:3:36 | call to field |
| app/graphql/types/post.rb:1:1:6:5 | Post | app/graphql/types/post.rb:4:5:4:60 | call to field |
| app/graphql/types/post.rb:1:1:6:5 | Post | app/graphql/types/post.rb:5:5:5:51 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:3:5:5:40 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:7:5:9:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:15:5:17:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:24:5:26:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:32:5:35:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:46:5:49:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:55:5:57:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:65:5:67:7 | call to field |
| app/graphql/types/query_type.rb:2:3:85:5 | QueryType | app/graphql/types/query_type.rb:72:5:76:7 | call to field |
graphqlResolveMethod
| app/graphql/mutations/dummy.rb:9:5:12:7 | resolve |
| app/graphql/resolvers/dummy_resolver.rb:10:5:13:7 | resolve |
Expand All @@ -23,24 +32,56 @@ graphqlLoadMethodRoutedParameter
| app/graphql/resolvers/dummy_resolver.rb:6:5:8:7 | load_something | app/graphql/resolvers/dummy_resolver.rb:6:24:6:25 | id |
graphqlFieldDefinitionMethodCall
| app/graphql/types/mutation_type.rb:3:5:3:44 | call to field |
| app/graphql/types/post.rb:2:5:2:24 | call to field |
| app/graphql/types/post.rb:3:5:3:36 | call to field |
| app/graphql/types/post.rb:4:5:4:60 | call to field |
| app/graphql/types/post.rb:5:5:5:51 | call to field |
| app/graphql/types/query_type.rb:3:5:5:40 | call to field |
| app/graphql/types/query_type.rb:7:5:9:7 | call to field |
| app/graphql/types/query_type.rb:15:5:17:7 | call to field |
| app/graphql/types/query_type.rb:24:5:26:7 | call to field |
| app/graphql/types/query_type.rb:32:5:35:7 | call to field |
| app/graphql/types/query_type.rb:46:5:49:7 | call to field |
| app/graphql/types/query_type.rb:55:5:57:7 | call to field |
| app/graphql/types/query_type.rb:65:5:67:7 | call to field |
| app/graphql/types/query_type.rb:72:5:76:7 | call to field |
graphqlFieldResolutionMethod
| app/graphql/types/query_type.rb:10:5:13:7 | with_arg |
| app/graphql/types/query_type.rb:18:5:22:7 | custom_method |
| app/graphql/types/query_type.rb:27:5:30:7 | with_splat |
| app/graphql/types/query_type.rb:36:5:40:7 | with_splat_and_named_arg |
| app/graphql/types/query_type.rb:50:5:53:7 | with_enum |
| app/graphql/types/query_type.rb:58:5:63:7 | with_nested_enum |
| app/graphql/types/query_type.rb:68:5:70:7 | with_array |
| app/graphql/types/query_type.rb:77:5:84:7 | with_named_params |
graphqlFieldResolutionRoutedParameter
| app/graphql/types/query_type.rb:10:5:13:7 | with_arg | app/graphql/types/query_type.rb:10:18:10:23 | number |
| app/graphql/types/query_type.rb:18:5:22:7 | custom_method | app/graphql/types/query_type.rb:18:23:18:33 | blah_number |
| app/graphql/types/query_type.rb:27:5:30:7 | with_splat | app/graphql/types/query_type.rb:27:20:27:25 | **args |
| app/graphql/types/query_type.rb:36:5:40:7 | with_splat_and_named_arg | app/graphql/types/query_type.rb:36:34:36:37 | arg1 |
| app/graphql/types/query_type.rb:36:5:40:7 | with_splat_and_named_arg | app/graphql/types/query_type.rb:36:41:36:46 | **rest |
| app/graphql/types/query_type.rb:68:5:70:7 | with_array | app/graphql/types/query_type.rb:68:20:68:23 | list |
| app/graphql/types/query_type.rb:77:5:84:7 | with_named_params | app/graphql/types/query_type.rb:77:27:77:30 | arg1 |
graphqlFieldResolutionDefinition
| app/graphql/types/query_type.rb:10:5:13:7 | with_arg | app/graphql/types/query_type.rb:7:5:9:7 | call to field |
| app/graphql/types/query_type.rb:18:5:22:7 | custom_method | app/graphql/types/query_type.rb:15:5:17:7 | call to field |
| app/graphql/types/query_type.rb:27:5:30:7 | with_splat | app/graphql/types/query_type.rb:24:5:26:7 | call to field |
| app/graphql/types/query_type.rb:36:5:40:7 | with_splat_and_named_arg | app/graphql/types/query_type.rb:32:5:35:7 | call to field |
| app/graphql/types/query_type.rb:50:5:53:7 | with_enum | app/graphql/types/query_type.rb:46:5:49:7 | call to field |
| app/graphql/types/query_type.rb:58:5:63:7 | with_nested_enum | app/graphql/types/query_type.rb:55:5:57:7 | call to field |
| app/graphql/types/query_type.rb:68:5:70:7 | with_array | app/graphql/types/query_type.rb:65:5:67:7 | call to field |
| app/graphql/types/query_type.rb:77:5:84:7 | with_named_params | app/graphql/types/query_type.rb:72:5:76:7 | call to field |
graphqlRemoteFlowSources
| app/graphql/mutations/dummy.rb:5:24:5:25 | id |
| app/graphql/mutations/dummy.rb:9:17:9:25 | something |
| app/graphql/resolvers/dummy_resolver.rb:6:24:6:25 | id |
| app/graphql/resolvers/dummy_resolver.rb:10:17:10:25 | something |
| app/graphql/types/query_type.rb:10:18:10:23 | number |
| app/graphql/types/query_type.rb:18:23:18:33 | blah_number |
| app/graphql/types/query_type.rb:28:22:28:37 | ...[...] |
| app/graphql/types/query_type.rb:29:7:29:22 | ...[...] |
| app/graphql/types/query_type.rb:36:34:36:37 | arg1 |
| app/graphql/types/query_type.rb:38:22:38:32 | ...[...] |
| app/graphql/types/query_type.rb:52:22:52:32 | ...[...] |
| app/graphql/types/query_type.rb:60:22:60:41 | ...[...] |
| app/graphql/types/query_type.rb:68:20:68:23 | list |
| app/graphql/types/query_type.rb:77:27:77:30 | arg1 |
| app/graphql/types/query_type.rb:80:22:80:33 | ...[...] |
3 changes: 3 additions & 0 deletions ruby/ql/test/library-tests/frameworks/graphql/GraphQL.ql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
private import codeql.ruby.frameworks.GraphQL
private import codeql.ruby.AST
private import codeql.ruby.dataflow.RemoteFlowSources

query predicate graphqlSchemaObjectClass(GraphqlSchemaObjectClass cls) { any() }

Expand Down Expand Up @@ -34,3 +35,5 @@ query predicate graphqlFieldResolutionDefinition(
) {
meth.getDefinition() = def
}

query predicate graphqlRemoteFlowSources(RemoteFlowSource src) { any() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Types::BaseEnum < GraphQL::Schema::Enum
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Types
class Direction < Types::BaseEnum
value "asc", "Ascending order", value: "asc"
value "desc", "Descending order", value: "desc"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Types
class MediaCategory < Types::BaseEnum
value "AUDIO", "An audio file, such as music or spoken word"
value "IMAGE", "A still image, such as a photo or graphic"
value "TEXT", "Written words"
value "VIDEO", "Motion picture, may have audio"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Types::Post < GraphQL::Schema::Object
field :title, String
field :body, String, null: false
field :media_category, Types::MediaCategory, null: false
field :direction, Types::Direction, null: false
end
end

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Types
class PostOrder < Types::BaseInputObject
argument :direction, Types::Direction, "The ordering direction", required: true
end
end
Loading

0 comments on commit 1297acf

Please sign in to comment.