Skip to content

Commit

Permalink
Merge pull request #3167 from jturkel/feature/sanitization-improvements
Browse files Browse the repository at this point in the history
Make query sanitization more extensible
  • Loading branch information
Robert Mosolgo authored Sep 29, 2020
2 parents 1b7f36d + 7366d90 commit 462ba26
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 20 deletions.
56 changes: 40 additions & 16 deletions lib/graphql/language/sanitized_printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ class SanitizedPrinter < GraphQL::Language::Printer

REDACTED = "\"<REDACTED>\""

def initialize(query)
def initialize(query, inline_variables: true)
@query = query
@current_type = nil
@current_field = nil
@current_input_type = nil
@inline_variables = inline_variables
end

# @return [String, nil] A scrubbed query string, if the query was valid.
Expand All @@ -36,15 +37,14 @@ def sanitized_query_string
end

def print_node(node, indent: "")
if node.is_a?(String)
type = @current_input_type.unwrap
# Replace any strings that aren't IDs or Enum values with REDACTED
if type.kind.enum? || type.graphql_name == "ID"
super
case node
when FalseClass, Float, Integer, String, TrueClass
if @current_argument && redact_argument_value?(@current_argument, node)
redacted_argument_value(@current_argument)
else
REDACTED
super
end
elsif node.is_a?(Array)
when Array
old_input_type = @current_input_type
if @current_input_type && @current_input_type.list?
@current_input_type = @current_input_type.of_type
Expand All @@ -59,15 +59,30 @@ def print_node(node, indent: "")
end
end

# Indicates whether or not to redact non-null values for the given argument. Defaults to redacting all strings
# arguments but this can be customized by subclasses.
def redact_argument_value?(argument, value)
# Default to redacting any strings or custom scalars encoded as strings
type = argument.type.unwrap
value.is_a?(String) && type.kind.scalar? && (type.graphql_name == "String" || !type.default_scalar?)
end

# Returns the value to use for redacted versions of the given argument. Defaults to the
# string "<REDACTED>".
def redacted_argument_value(argument)
REDACTED
end

def print_argument(argument)
# We won't have type information if we're recursing into a custom scalar
return super if @current_input_type && @current_input_type.kind.scalar?

arg_owner = @current_input_type || @current_directive || @current_field
arg_def = arg_owner.arguments[argument.name]
old_current_argument = @current_argument
@current_argument = arg_owner.arguments[argument.name]

old_input_type = @current_input_type
@current_input_type = arg_def.type.non_null? ? arg_def.type.of_type : arg_def.type
@current_input_type = @current_argument.type.non_null? ? @current_argument.type.of_type : @current_argument.type

argument_value = if coerce_argument_value_to_list?(@current_input_type, argument.value)
[argument.value]
Expand All @@ -77,6 +92,7 @@ def print_argument(argument)
res = "#{argument.name}: #{print_node(argument_value)}".dup

@current_input_type = old_input_type
@current_argument = old_current_argument
res
end

Expand All @@ -88,8 +104,12 @@ def coerce_argument_value_to_list?(type, value)
end

def print_variable_identifier(variable_id)
variable_value = query.variables[variable_id.name]
print_node(value_to_ast(variable_value, @current_input_type))
if @inline_variables
variable_value = query.variables[variable_id.name]
print_node(value_to_ast(variable_value, @current_input_type))
else
super
end
end

def print_field(field, indent: "")
Expand Down Expand Up @@ -141,10 +161,14 @@ def print_operation_definition(operation_definition, indent: "")
old_type = @current_type
@current_type = query.schema.public_send(operation_definition.operation_type)

out = "#{indent}#{operation_definition.operation_type}".dup
out << " #{operation_definition.name}" if operation_definition.name
out << print_directives(operation_definition.directives)
out << print_selections(operation_definition.selections, indent: indent)
if @inline_variables
out = "#{indent}#{operation_definition.operation_type}".dup
out << " #{operation_definition.name}" if operation_definition.name
out << print_directives(operation_definition.directives)
out << print_selections(operation_definition.selections, indent: indent)
else
out = super
end

@current_type = old_type
out
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,9 @@ def arguments_for(ast_node, definition, parent_object: nil)
# - Variables inlined to the query
# - Strings replaced with `<REDACTED>`
# @return [String, nil] Returns nil if the query is invalid.
def sanitized_query_string
def sanitized_query_string(inline_variables: true)
with_prepared_ast {
GraphQL::Language::SanitizedPrinter.new(self).sanitized_query_string
GraphQL::Language::SanitizedPrinter.new(self, inline_variables: inline_variables).sanitized_query_string
}
end

Expand Down
92 changes: 90 additions & 2 deletions spec/graphql/language/sanitized_printer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@ class Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
end

class CustomSanitizedPrinter < GraphQL::Language::SanitizedPrinter
def redact_argument_value?(argument, value)
true
end

def redacted_argument_value(argument)
"<#{argument.graphql_name}-redacted>"
end
end
end

def sanitize_string(query_string, **options)
def sanitize_string(query_string, inline_variables: true, **options)
query = GraphQL::Query.new(
SanitizeTest::Schema,
query_string,
**options
)
query.sanitized_query_string
query.sanitized_query_string(inline_variables: inline_variables)
end

it "replaces strings with redacted" do
Expand Down Expand Up @@ -141,6 +151,52 @@ def sanitize_string(query_string, **options)
assert_equal expected_query_string, sanitize_string(query_str, variables: variables)
end

it "doesn't inline variables when inline_variables is false" do
query_str = '
query($string1: String!, $string2: String = "str2", $inputObject: ExampleInput!, $strings: [String!]!) {
inputs(
string: $string1,
id: "id1",
int: 1,
float: 1.0,
url: "http://graphqliscool.com",
enum: RED
inputObject: {
string: $string2
id: "id2"
int: 2
float: 2.0
url: "http://graphqliscool.com"
enum: RED
inputObject: $inputObject
}
)
strings(strings: $strings)
}
'

variables = {
"string1" => "str1",
"strings" => ["str1", "str2"],
"inputObject" => {
"string" => "str3",
"id" => "id3",
"int" => 3,
"float" => 3.3,
"url" => "three.com",
"enum" => "BLUE"
}
}

expected_query_string = 'query($string1: String!, $string2: String = "str2", $inputObject: ExampleInput!, $strings: [String!]!) {
inputs(' +
'string: $string1, id: "id1", int: 1, float: 1.0, url: "<REDACTED>", enum: RED, inputObject: {' +
'string: $string2, id: "id2", int: 2, float: 2.0, url: "<REDACTED>", enum: RED, inputObject: $inputObject})
strings(strings: $strings)
}'
assert_equal expected_query_string, sanitize_string(query_str, variables: variables, inline_variables: false)
end

it "redacts from lists" do
query_str_1 = '{ strings(strings: ["s1", "s2"]) }'
query_str_2 = 'query($strings: [String!]!) { strings(strings: $strings) }'
Expand Down Expand Up @@ -222,5 +278,37 @@ def sanitize_string(query_string, **options)
it "returns nil on invalid queries" do
assert_nil sanitize_string "{ __typename "
end

it "provides hooks to override the redaction behavior" do
query_str = '
{
inputs(
string: "string",
id: "id",
int: 1,
float: 2.0,
url: "http://graphqliscool.com",
enum: RED
inputObject: {
string: "string"
id: "id"
int: 1
float: 2.0
url: "http://graphqliscool.com"
enum: RED
}
)
}
'

expected_query_string = 'query {
inputs(' +
'string: <string-redacted>, id: <id-redacted>, int: <int-redacted>, float: <float-redacted>, url: <url-redacted>, enum: RED, inputObject: {' +
'string: <string-redacted>, id: <id-redacted>, int: <int-redacted>, float: <float-redacted>, url: <url-redacted>, enum: RED})
}'
query = GraphQL::Query.new(SanitizeTest::Schema, query_str)
sanitized_query = SanitizeTest::CustomSanitizedPrinter.new(query).sanitized_query_string
assert_equal expected_query_string, sanitized_query
end
end

0 comments on commit 462ba26

Please sign in to comment.