How to escape inputs in AppSync resolvers

VTL makes injection-like attacks possible

Author's image
Tamás Sallai
4 mins

VTL with JSON

AppSync resolvers use VTL, a general-purpose templating language. During the transformation it gets a template file and a context variable then produces text. AppSync then parses this text as JSON and calls the data source with it (in the case of a request mapping template) or returns to the GraphQL query (for response mapping templates).

Using VTL to produce JSON makes the template easily readable for simple cases. For example, getting an item from DynamoDB based on an id argument:

{
	"version": "2018-05-29",
	"operation": "GetItem",
	"key": {
		"id": {"S": $util.toJson($ctx.args.id)}
	},
	"consistentRead": true
}

But there is a problem behind this: as VTL produces text that is parsed as JSON, this opens the possibility for injection-type attacks. At best, it breaks the resolver for some inputs, at worst, it opens security vulnerabilities.

Similarities to SQL injection

SQL injection is possible because of a similar problem: code and data are sent together and produced using a process that understands only text. A backend constructs the text that is interpreted by a database and if it is not careful enough a specially constructed input can insert code and not only data.

An example from wikipedia:

var statement = "SELECT * FROM users WHERE name = '" + userName + "'";

The above code works fine for "normal" inputs, such as susan, which is only data. But the naive concatenation allows inserting code:

' OR '1'='1

The output:

SELECT * FROM users WHERE name = '' OR '1'='1';

By allowing ', the input can "break out" of the "data world" and enter the "code world".

AppSync resolvers behave similarly: if VTL inserts a variable that is not properly escaped, it can modify the JSON structure.

In the case of SQL injection, the solution was to separate code from data in the form of prepared statements. In a prepared statement the variables are just placeholders in the code and then sent separately.

Unfortunately, AppSync offers no support for a mechanism like that. So the only approach is proper escaping using the $util.toJson() function.

Unescaped variables

First, let's see how injection works in VTL templates! For this test case, we'll use a resolver with the NONE data source with a payload that comes directly from an argument:

{
	"version": "2018-05-29",
	"payload": "$ctx.args.input"
}

As the $ctx.args.input is a String, this works for simple inputs:

query MyQuery {
	unescaped(input: "test")  
}

The result:

{
	"data": {
		"unescaped": "test"
	}
}

But what happens when the argument is missing? It can happen when it is marked as optional in the schema.

query MyQuery {
	unescaped  
}
{
	"data": {
		"unescaped": "$ctx.args.input"
	}
}

VTL inserts the variable name if it can't resolve the value, which is usually not what the expected result.

And finally, let's see if the input contains a JSON control character!

query MyQuery {
	unescaped(input: "\"")
}

This is an error:

{
	"data": {
		"unescaped": null
	},
	"errors": [
		{
			"path": [
				"unescaped"
			],
			"data": null,
			"errorType": "MappingTemplate",
			"errorInfo": null,
			"locations": [
				{
					"line": 6,
					"column": 3,
					"sourceName": null
				}
			],
			"message": "Unexpected character ('\"' (code 34)): was expecting comma to separate Object entries\n at [Source: (String)\"{\n\t\"version\": \"2018-05-29\",\n\t\"payload\": \"\"\"\n}\n\"; line: 3, column: 16]"
		}
	]
}

Escaping

Let's add escaping to the resolver:

{
	"version": "2018-05-29",
	"payload": $util.toJson($ctx.args.input)
}

Then do the previous tests:

query MyQuery {
	basic_e: escaped(input: "test")
	missing_e: escaped
	quote_e: escaped(input: "\"")
}

The results:

{
	"data": {
		"basic_e": "test",
		"missing_e": null,
		"quote_e": "\""
	}
}

Conclusion

Escaping it important in AppSync VTL as it produces text that is then interpreted as JSON. Failure to do that often produces incorrect responses and errors, and in some cases opens security vulnerabilities.

This is similar to SQL injection, but contrast to SQL, the data sources allow limited customizations so it rarely lead to security problems.

Make sure you always use the $util.toJson function to escape the variables in the templates.

November 1, 2022

Free PDF guide

Sign up to our newsletter and download the "Foreign key constraints in DynamoDB" guide.


In this article