Trusted Documents
RedwoodJS can be setup to enforce persisted operations – alternatively called Trusted Documents.
Use trusted documents if your GraphQL API is only for your own app (which is the case for most GraphQL APIs) for a massively decreased attack-surface, increased performance, and decreased bandwidth usage.
At app build time, Redwood will extract the GraphQL documents (queries, etc) and make them available to the server. At run time, you can then send "document id" or "hash" instead of the whole document; only accept requests with a known document id.
This prevents malicious attackers from executing arbitrary GraphQL thus helping with unwanted resolver traversal or information leaking.
See Configure Trusted Documents for more information and usage instructions.
Trusted Documents Explained
When configured to use Trusted Documents, your project will:
- When generating types, generate files in
web/src/graphql
needed for persisted aka trusted documents, for example:
{
"4dd4c49aef34e20af52efb50a1d0ebb0b8062b6d": "query FindAuthorQuery($id: Int!) { __typename author: user(id: $id) { __typename email fullName } }",
"46e9823d95110ebb2ef17ef82fff5c19a468f8a6": "query FindBlogPostQuery($id: Int!) { __typename blogPost: post(id: $id) { __typename author { __typename email fullName } body createdAt id title } }",
"421bcffdde84d448ec1a1b30b36eaeb966f00257": "query BlogPostsQuery { __typename blogPosts: posts { __typename author { __typename email fullName } body createdAt id title } }",
"f6ae606548009c2cd4c69b9aecebad0a730ba23d": "mutation DeleteContactMutation($id: Int!) { __typename deleteContact(id: $id) { __typename id } }",
"f7d2df28fcf87b0c29d225df79363d1c69159916": "query FindContactById($id: Int!) { __typename contact: contact(id: $id) { __typename createdAt email id message name } }",
"7af93a7e454d9c59bbb77c14e0c78e99207fd0c6": "query FindContacts { __typename contacts { __typename createdAt email id message name } }",
"e01ad8e899ac908458eac2d1f989b88160a0494b": "query EditContactById($id: Int!) { __typename contact: contact(id: $id) { __typename createdAt email id message name } }",
"94f51784b918a52e9af64f3c1fd4356903b611f8": "mutation UpdateContactMutation($id: Int!, $input: UpdateContactInput!) { __typename updateContact(id: $id, input: $input) { __typename createdAt email id message name } }",
"da35778949e1e8e27b7d1bb6b2a630749c5d7060": "mutation CreateContactMutation($input: CreateContactInput!) { __typename createContact(input: $input) { __typename id } }",
"4f880f909a16b7fe15898fe33a2ee26933466719": "query EditPostById($id: Int!) { __typename post: post(id: $id) { __typename authorId body createdAt id title } }",
"32b9225df81ff7845fedfa6d5c86c5d4a76073d2": "mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) { __typename updatePost(id: $id, input: $input) { __typename authorId body createdAt id title } }",
"daf229dcea085f1beff91102a63c2ba9c88e8481": "mutation CreatePostMutation($input: CreatePostInput!) { __typename createPost(input: $input) { __typename id } }",
"e3405f6dcb6460943dd604423f0f517bc8318aaa": "mutation DeletePostMutation($id: Int!) { __typename deletePost(id: $id) { __typename id } }",
"43a94ad9a150aa7a7a665c73a931a5b18b6cc28b": "query FindPostById($id: Int!) { __typename post: post(id: $id) { __typename authorId body createdAt id title } }",
"76308e971322b1ece4cdff75185bb61d7139e343": "query FindPosts { __typename posts { __typename authorId body createdAt id title } }",
"287beba179ef2c4448b4d3b150701993eddc07d6": "query BlogPostsQueryTrustedPage { __typename blogPosts: posts { __typename author { __typename email fullName } body createdAt id title } }"
}
- They contain the query and hash that represents and identifies that query
- Files with functions to lookup the generated trusted document such as:
// ...
export function graphql(
source: "\n query FindPosts {\n posts {\n id\n title\n body\n authorId\n createdAt\n }\n }\n"
): (typeof documents)["\n query FindPosts {\n posts {\n id\n title\n body\n authorId\n createdAt\n }\n }\n"];
// ...
export function gql(source: string) {
return graphql(source);
}
and the generated AST with the hash id in web/src/graphql/graphql.ts
// ...
export const FindPostsDocument = {"__meta__":{"hash":"76308e971322b1ece4cdff75185bb61d7139e343"},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindPosts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"posts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<FindPostsQuery, FindPostsQueryVariables>;
// ...
so that when a query or mutation is made, the web side GraphQL client doesn't send the query, but rather just the hash id so that the GraphQL Server can lookup the pre-generated query to run.
{"operationName":"FindPosts","variables":{},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"76308e971322b1ece4cdff75185bb61d7139e343"}}}
It does so by adding a api/src/lib/trustedDocumentsStore.ts
file for use on the GraphQL api side.
export const store = {
// ...
'76308e971322b1ece4cdff75185bb61d7139e343':
'query FindPosts { __typename posts { __typename authorId body createdAt id title } }',
// ...
}
See how the 76308e971322b1ece4cdff75185bb61d7139e343
hash ids match?
Now, when the client requests to make a query for 76308e971322b1ece4cdff75185bb61d7139e343
, the GraphQL server knows to execute the corresponding query associated with that hash.
This means that because queries are pre-generated and the hash ids *must match, there is no way for any un-trusted or ad-hock queries to get executed by the GraphQL server.
Thus preventing unwanted queries or GraphQl traversal attacks,
- Configure RedwoodJS to use Trusted Documents via
redwood.toml
- Configure the GraphQL Server
Configure Trusted Documents
Below are instructions to manually configure Trusted Documents in your RedwoodJS project.
Alternatively, you can use the yarn redwood setup graphql trusted-documents
CLI setup command.
Configure redwood.toml
Setting trustedDocuments
to true will
- populate
web/src/graphql
files with the pre-generated documents - inform Apollo GraphQL client to send the document hashes and not the query itself
...
[graphql]
trustedDocuments = true
...
Configure GraphQL Handler
As part of GraphQL type and codegen, the trustedDocumentsStore
is created in api/src/lib
.
This is the same information that is created in web/src/graphql/persisted-documents.json
but wrapped in a store
that can be easily imported and passed to the GraphQL Handler.
To enable trusted documents, configure trustedDocuments
with the store.
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
// ...
import { store } from 'src/lib/trustedDocumentsStore'
export const handler = createGraphQLHandler({
getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
trustedDocuments: { store },
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
If you'd like to customize the message when a query is not permitted, you can set the persistedQueryOnly
configuration setting in customErrors
:
trustedDocuments: {
store,
customErrors: {
persistedQueryOnly: 'This ad-hoc query is not allowed.'
},
}