diff --git a/ruby/ql/lib/change-notes/2023-09-18-graphql-sources.md b/ruby/ql/lib/change-notes/2023-09-18-graphql-sources.md new file mode 100644 index 000000000000..70f065cee129 --- /dev/null +++ b/ruby/ql/lib/change-notes/2023-09-18-graphql-sources.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* GraphQL enums are no longer considered remote flow sources. \ No newline at end of file diff --git a/ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll b/ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll index 98fbe241404e..a335f406ae00 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll @@ -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() } } /** @@ -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() } } /** @@ -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()) ) } @@ -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" } +} diff --git a/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.expected b/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.expected index 173f3846f9ac..3a8e4a6123c1 100644 --- a/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.expected +++ b/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.expected @@ -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 | @@ -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 | ...[...] | diff --git a/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.ql b/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.ql index b443d9232220..ef42bf757c06 100644 --- a/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.ql +++ b/ruby/ql/test/library-tests/frameworks/graphql/GraphQL.ql @@ -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() } @@ -34,3 +35,5 @@ query predicate graphqlFieldResolutionDefinition( ) { meth.getDefinition() = def } + +query predicate graphqlRemoteFlowSources(RemoteFlowSource src) { any() } diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/base_enum.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/base_enum.rb new file mode 100644 index 000000000000..ca89ec705a5f --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/base_enum.rb @@ -0,0 +1,2 @@ +class Types::BaseEnum < GraphQL::Schema::Enum +end \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/direction.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/direction.rb new file mode 100644 index 000000000000..9cc58cf1d937 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/direction.rb @@ -0,0 +1,6 @@ +module Types + class Direction < Types::BaseEnum + value "asc", "Ascending order", value: "asc" + value "desc", "Descending order", value: "desc" + end +end \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/media_category.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/media_category.rb new file mode 100644 index 000000000000..3a97a919c41c --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/media_category.rb @@ -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 \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post.rb new file mode 100644 index 000000000000..316d29ff16c1 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post.rb @@ -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 + \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post_order.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post_order.rb new file mode 100644 index 000000000000..4ec6b3edc7f0 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/post_order.rb @@ -0,0 +1,5 @@ +module Types + class PostOrder < Types::BaseInputObject + argument :direction, Types::Direction, "The ordering direction", required: true + end +end \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/query_type.rb b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/query_type.rb index e0bc578a9116..9d79d8088cfd 100644 --- a/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/query_type.rb +++ b/ruby/ql/test/library-tests/frameworks/graphql/app/graphql/types/query_type.rb @@ -42,5 +42,45 @@ def with_splat_and_named_arg(arg1:, **rest) def foo(arg) system("echo #{arg}") end + + field :with_enum, String, null: false, description: "A field with an enum argument" do + argument :enum, Types::MediaCategory, "An enum", required: true + argument :arg2, String, "Another arg", required: true + end + def with_enum(**args) + system("echo #{args[:enum]}") + system("echo #{args[:arg2]}") + end + + field :with_nested_enum, String, null: false, description: "A field with a nested enum argument" do + argument :inner, Types::Post, "Post", required: true + end + def with_nested_enum(**args) + system("echo #{args[:inner]}") + system("echo #{args[:inner][:title]}") + system("echo #{args[:inner][:media_category]}") + system("echo #{args[:inner][:direction]}") + end + + field :with_array, String do + argument :list, [String], "Names" + end + def with_array(list:) + system("echo #{list[0]}") + end + + field :with_named_params, String do + argument :arg1, String, "Arg 1" + argument :arg2, Types::Post, "Arg 2" + argument :arg3, Types::MediaCategory, "Arg 3" + end + def with_named_params(arg1:, arg2:, **args) + system("echo #{arg1}") + system("echo #{arg2}") + system("echo #{arg2[:title]}") + system("echo #{arg2[:media_category]}") + system("echo #{args[:arg3]}") + system("echo #{args[:not_an_arg]}") + end end end diff --git a/ruby/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected b/ruby/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected index ba0fef88b62e..24ae9c623200 100644 --- a/ruby/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected +++ b/ruby/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected @@ -17,8 +17,6 @@ edges | CommandInjection.rb:54:13:54:24 | ...[...] | CommandInjection.rb:54:7:54:9 | cmd | | CommandInjection.rb:73:18:73:23 | number | CommandInjection.rb:74:14:74:29 | "echo #{...}" | | CommandInjection.rb:81:23:81:33 | blah_number | CommandInjection.rb:82:14:82:34 | "echo #{...}" | -| CommandInjection.rb:90:20:90:25 | **args | CommandInjection.rb:91:22:91:25 | args | -| CommandInjection.rb:91:22:91:25 | args | CommandInjection.rb:91:22:91:37 | ...[...] | | CommandInjection.rb:91:22:91:37 | ...[...] | CommandInjection.rb:91:14:91:39 | "echo #{...}" | | CommandInjection.rb:103:9:103:12 | file | CommandInjection.rb:104:16:104:28 | "cat #{...}" | | CommandInjection.rb:103:16:103:21 | call to params | CommandInjection.rb:103:16:103:28 | ...[...] | @@ -47,9 +45,7 @@ nodes | CommandInjection.rb:74:14:74:29 | "echo #{...}" | semmle.label | "echo #{...}" | | CommandInjection.rb:81:23:81:33 | blah_number | semmle.label | blah_number | | CommandInjection.rb:82:14:82:34 | "echo #{...}" | semmle.label | "echo #{...}" | -| CommandInjection.rb:90:20:90:25 | **args | semmle.label | **args | | CommandInjection.rb:91:14:91:39 | "echo #{...}" | semmle.label | "echo #{...}" | -| CommandInjection.rb:91:22:91:25 | args | semmle.label | args | | CommandInjection.rb:91:22:91:37 | ...[...] | semmle.label | ...[...] | | CommandInjection.rb:103:9:103:12 | file | semmle.label | file | | CommandInjection.rb:103:16:103:21 | call to params | semmle.label | call to params | @@ -69,5 +65,5 @@ subpaths | CommandInjection.rb:59:14:59:16 | cmd | CommandInjection.rb:54:13:54:18 | call to params | CommandInjection.rb:59:14:59:16 | cmd | This command depends on a $@. | CommandInjection.rb:54:13:54:18 | call to params | user-provided value | | CommandInjection.rb:74:14:74:29 | "echo #{...}" | CommandInjection.rb:73:18:73:23 | number | CommandInjection.rb:74:14:74:29 | "echo #{...}" | This command depends on a $@. | CommandInjection.rb:73:18:73:23 | number | user-provided value | | CommandInjection.rb:82:14:82:34 | "echo #{...}" | CommandInjection.rb:81:23:81:33 | blah_number | CommandInjection.rb:82:14:82:34 | "echo #{...}" | This command depends on a $@. | CommandInjection.rb:81:23:81:33 | blah_number | user-provided value | -| CommandInjection.rb:91:14:91:39 | "echo #{...}" | CommandInjection.rb:90:20:90:25 | **args | CommandInjection.rb:91:14:91:39 | "echo #{...}" | This command depends on a $@. | CommandInjection.rb:90:20:90:25 | **args | user-provided value | +| CommandInjection.rb:91:14:91:39 | "echo #{...}" | CommandInjection.rb:91:22:91:37 | ...[...] | CommandInjection.rb:91:14:91:39 | "echo #{...}" | This command depends on a $@. | CommandInjection.rb:91:22:91:37 | ...[...] | user-provided value | | CommandInjection.rb:104:16:104:28 | "cat #{...}" | CommandInjection.rb:103:16:103:21 | call to params | CommandInjection.rb:104:16:104:28 | "cat #{...}" | This command depends on a $@. | CommandInjection.rb:103:16:103:21 | call to params | user-provided value |