Is input validation too loose for String and Boolean?
See original GitHub issueThe QA team here is asking me about the validation of Boolean input values. I need to give them an answer.
The GraphQL specification for input Boolean and String values states: Reference: http://facebook.github.io/graphql/October2016/#sec-Boolean-Value
2.9.3 Boolean Value
BooleanValue: one of
true false
The two keywords true and false represent the two boolean values.
2.9.4 String Value
StringValue ::
""
" StringCharacter list "
[excerpted]
Strings are sequences of characters wrapped in double‐quotes ("). (ex. "Hello World").
White space and other otherwise‐ignored characters are significant within a string value.
Is this the reference specification used for the graphql-java implementation?
The same or a similar issue was recently addressed in graphql-js here: https://github.com/graphql/graphql-js/issues/771
Example follows. The output is generated by the graphql java 6.0 release. Schema Definition:
schema { query : Query }
type Query { ping ( input : Param ! ) : Ping }
input Param {
s : String !
b : Boolean !
}
type Ping {
s : String
b : Boolean
}
Sample requests and program output:
R-1:
{
"query" : "query ( $input : Param ! ) { ping ( input : $input ) { s b }}"
"variables" : {
"input" : {
"s" : 0
"b" : 0
}
}
}
Output: {“data”:{“ping”:{“s”:“0”,“b”:false}}}
R-2:
{
"query" : "query ( $input : Param ! ) { ping ( input : $input ) { s b }}"
"variables" : {
"input" : {
"s" : 123345,
"b" : 123345
}
}
}
Output: {“data”:{“ping”:{“s”:“12345”,“b”:true}}}
R-3:
{
"query" : "query ( $input : Param ! ) { ping ( input : $input ) { s b } }"
"variables" : {
"input" : {
"s" : "Atlanta",
"b" : "Atlanta"
}
}
}
Output: {“data”:{“ping”:{“s”:“Atlanta”,“b”:false}}}
R-4:
{
"query" : "query ( $input : Param ! ) { ping ( input : $input ) { s b } }"
"variables" : {
"input" : {
"s" : [1,2,3],
"b" : "Atlanta"
}
}
}
Output: {“data”:{“ping”:{“s”:“[1, 2, 3]”,“b”:false}}}
R-5:
{
"query" : "query ( $input : Param ! ) { ping ( input : $input ) { s b } }"
"variables" : {
"input" : {
"s" : [1,2,3],
"b" : [1,2,3]
}
}
}
Output: {“errors”:[{“message”:“Invalid input ‘[1, 2, 3]’ for Boolean”}]}
Output Expectations: R-1, R-2 should return an error for either s or b because the value of s is not quoted and the value of b is neither true nor false. R-3 should return return an error for b because the value of b is neither true nor false. R-4 should return an error for s because the value of s is not quoted R-5 is ok because an error is returned for b as expected; but depending on the order of validation an error for s might be returned instead.
The Java 8 Program used for testing:
package graphqltest;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.TypeInfo;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class GraphQLTest {
public static void main(String[] args) {
SchemaParser parser = new SchemaParser();
SchemaGenerator generator = new SchemaGenerator();
TypeDefinitionRegistry registry = parser.parse("schema { query : Query } type Query { ping ( input : Param ! ) : Ping } input Param { s : String ! b : Boolean ! } type Ping{ s : String b : Boolean }");
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring().type("Query", wires -> wires.dataFetcher("ping", env -> {
if (env.containsArgument("input")) { return env.getArgument("input"); }
return null;
})).build();
GraphQLSchema schema = generator.makeExecutableSchema(registry, wiring);
GraphQL gql = GraphQL.newGraphQL(schema).build();
ObjectMapper mapper = new ObjectMapper();
SimpleModule mod = new SimpleModule();
mod.addSerializer(TypeInfo.class, new JsonSerializer<TypeInfo>() {
@Override
public void serialize(TypeInfo t, JsonGenerator jg, SerializerProvider sp) throws IOException, JsonProcessingException { jg.writeString(t.toString()); }
});
mapper.registerModule(mod);
HashMapTypeReference typeRef = new HashMapTypeReference();
String[] requests = new String[]{
"{\"query\":\"query ( $input : Param ! ) { ping ( input : $input ) { s b }}\",\"variables\":{\"input\":{\"s\":0,\"b\":0}}}", // R-1
"{\"query\":\"query ( $input : Param ! ) { ping ( input : $input ) { s b }}\",\"variables\":{\"input\":{\"s\":12345,\"b\":12345}}}", // R-2
"{\"query\":\"query ( $input : Param ! ) { ping ( input : $input ) { s b }}\",\"variables\":{\"input\":{\"s\":\"Atlanta\",\"b\":\"Atlanta\"}}}", // R-3
"{\"query\":\"query ( $input : Param ! ) { ping ( input : $input ) { s b }}\",\"variables\":{\"input\":{\"s\":[1,2,3],\"b\":\"Atlanta\"}}}", // R-4
"{\"query\":\"query ( $input : Param ! ) { ping ( input : $input ) { s b }}\",\"variables\":{\"input\":{\"s\":[1,2,3],\"b\":[1,2,3]}}}" // R-5
};
for (String request : requests) {
try {
Map<String, Object> input = mapper.readValue(request, typeRef);
String query = (String) input.get("query");
Map<String, Object> variables = (Map<String, Object>) input.get("variables");
ExecutionResult result = gql.execute(ExecutionInput.newExecutionInput().query(query).variables(variables).build());
System.out.println("----------");
System.out.println("Request: " + request);
System.out.println(" Output: " + mapper.writeValueAsString(result.toSpecification()));
} catch (Exception ex) {
System.out.println(ex);
}
}
System.out.append("----------");
}
@SuppressWarnings("PublicInnerClass")
public static class HashMapTypeReference extends TypeReference<HashMap<String, Object>> { }
}
Libraries:
antlr-runtime-4.7.jar
graphql-java-6.0.jar
jackson-annotations-2.9.2.jar
jackson-core-2.9.2.jar
jackson-databind-2.9.2.jar
java-dataloader-2.0.1.jar
log4j-api-2.8.2.jar
log4j-core-2.8.2.jar
log4j-slf4j-impl-2.8.2.jar
slf4j-api-1.7.25.jar
Issue Analytics
- State:
- Created 6 years ago
- Comments:6 (3 by maintainers)
Thank you for looking into this very difficult and potentially contentious issue.
I will look into how to do the pull request and making groovy spock tests for reporting future issues. Thank you for the suggestion.
My thinking is that when the input data is JSON, the Map<String, Object> received as Input should have already been parsed by the JSON library’s (such as Jackson) string to Map<String, Object> parser and converted into the Java type as specified in the input data and JSON value conversion rules. So in the case of correct value syntax, there should be no need for value coercion.
For example: JSON:
{ "s": "s", "b": true, "n": 123 }
should be parsed into Java Map<String, Object>{ "s": String("s"), "b": Boolean.TRUE, "n": Integer(123) }
As a result, the graphql-java library could require that the values of built-in scalars in the Input Map be the type specified by the schema, since the built-in scalars are basically the same as those defined by the JSON format. Though there may need to be some leniency with numeric type conversions such as up casting (Int -> Long) when necessary.
This would mean that the code that prepares the variable map must do “the right thing”.
But that might not be current practice or not always reasonable, so making “strict” a requirement could break existing code and/or make parsing other data formats much more difficult. (i.e. web form data, Properties…)
My suggestion, would be (if the code will be modified) if it is feasible to make strict and lenient input validation selectable (with the default being lenient perhaps so as not to break anyone’s hard work). When explicitly set to “strict”, values in the input map must be the type specified by the schema without coercion. The “lenient” selection would provide the current (best effort to make sense out of the input) behavior.
This could be implemented in any of several different ways, for example:
closing this fairly old issue now . Please open a new issue against the current graphql java version if there is still a problem with input coercion. I am sorry that we didn’t address this topic in time.