├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── cost-limiter.png
├── cost-query.png
├── depth-limiter.png
├── evil-query.png
└── readme_logo.svg
├── deps.ts
├── example
└── opine.ts
├── mod.ts
├── src
├── guardeno.ts
├── protections
│ ├── cost-limiter.ts
│ ├── depth-limiter.ts
│ └── helper-functions.ts
└── types.ts
└── testing
├── cost-limiter-tests.ts
└── depth-limiter-tests.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .vscode
3 | .DS_Store
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish
4 | to make via issue, email, or any other method with the owners of this repository
5 | before making a change.
6 |
7 | Please note we have a code of conduct, please follow it in all your interactions
8 | with the project.
9 |
10 | ## Pull Request Process
11 |
12 | 1. Ensure any install or build dependencies are removed before the end of the
13 | layer when doing a build.
14 | 2. Update the README.md with details of changes to the interface, this includes
15 | new environment variables, exposed ports, useful file locations and container
16 | parameters.
17 | 3. You may merge the Pull Request in once you have the sign-off of two other
18 | developers, or if you do not have permission to do that, you may request the
19 | second reviewer to merge it for you.
20 |
21 | # Code of Conduct
22 |
23 | ## Our Pledge
24 |
25 | In the interest of fostering an open and welcoming environment, we as
26 | contributors and maintainers pledge to making participation in our project and
27 | our community a harassment-free experience for everyone, regardless of age, body
28 | size, disability, ethnicity, gender identity and expression, level of
29 | experience, nationality, personal appearance, race, religion, or sexual identity
30 | and orientation.
31 |
32 | ## Our Standards
33 |
34 | Examples of behavior that contributes to creating a positive environment
35 | include:
36 |
37 | - Using welcoming and inclusive language
38 | - Being respectful of differing viewpoints and experiences
39 | - Gracefully accepting constructive criticism
40 | - Focusing on what is best for the community
41 | - Showing empathy towards other community members
42 |
43 | Examples of unacceptable behavior by participants include:
44 |
45 | - The use of sexualized language or imagery and unwelcome sexual attention or
46 | advances
47 | - Trolling, insulting/derogatory comments, and personal or political attacks
48 | - Public or private harassment attacks
49 | - Publishing others' private information, such as a physical or electronic
50 | address, without explicit permission
51 | - Other conduct which could reasonably be considered inappropriate in a
52 | professional setting
53 |
54 | ## Our Responsibilities
55 |
56 | Project maintainers are responsible for clarifying the standards of acceptable
57 | behavior and are expected to take appropriate and fair corrective action in
58 | response to any instances of unacceptable behavior.
59 |
60 | Project maintainers have the right and responsibility to remove, edit, or reject
61 | comments, commits, code, wiki edits, issues, and other contributions that are
62 | not aligned to this Code of Conduct, or to ban temporarily or permanently any
63 | contributor for other behaviors that they deem inappropriate, threatening,
64 | offensive, or harmful.
65 |
66 | ## Scope
67 |
68 | This Code of Conduct applies both within project spaces and in public spaces
69 | when an individual is representing the project or its community. Examples of
70 | representing a project or community include using an official project e-mail
71 | address, posting via an official social media account, or acting as an appointed
72 | representative at an online or offline event. Representation of a project may be
73 | further defined and clarified by project maintainers.
74 |
75 | ## Enforcement
76 |
77 | All complaints will be reviewed and investigated and will result in a response
78 | that is deemed necessary and appropriate to the circumstances. The project team
79 | is obligated to maintain confidentiality with regard to the reporter of an
80 | incident. Further details of specific enforcement policies may be posted
81 | separately.
82 |
83 | Project maintainers who do not follow or enforce the Code of Conduct in good
84 | faith may face temporary or permanent repercussions as determined by other
85 | members of the project's leadership.
86 |
87 | ## Attribution
88 |
89 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
90 | available at http://contributor-covenant.org/version/1/4
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
GuarDenoQL
8 |
Simple and customizable security middleware for GraphQL servers in Deno
9 |
10 |
11 | # Features
12 |
13 | - Integrates with an Opine server in a Deno runtime.
14 | - Enables users to customize both a _**maximum depth**_ and a _**cost limit**_
15 | for all GraphQL queries and mutations sent to the server.
16 | - Validates queries and mutations against the depth limiter and/or cost limiter
17 | before they are executed by the server.
18 |
19 | # Why?
20 |
21 | ### **Depth Limiting**
22 |
23 | Because GraphQL schemas can be cyclic graphs, it is possible that a client could
24 | construct a query such as this one:
25 |
26 |
27 |
28 |
29 | Therefore, if nested deep enough, a malicious actor could potentially bring your server down with an abusive query.
30 |
31 |
32 |
33 | However, using a **Depth Limiter**, you can validate the depth of incoming
34 | queries against a user-defined limit and prevent these queries from going
35 | through.
36 |
37 | ### **Cost Limiting**
38 |
39 | Queries can still be very expensive even if they aren't nested deeply. Using a
40 | **Cost Limiter**, your server will calculate the total cost of the query based
41 | on its types before execution.
42 |
43 |
44 |
45 |
46 |
47 | # Getting Started
48 |
49 | A set up with [gql](https://github.com/deno-libs/gql) and
50 | [Opine](https://github.com/cmorten/opine) out-of-the-box:
51 |
52 | ```typescript
53 | import { opine, OpineRequest } from "https://deno.land/x/opine@2.2.0/mod.ts";
54 | import { GraphQLHTTP } from "https://deno.land/x/gql@1.1.2/mod.ts";
55 | import { makeExecutableSchema } from "https://deno.land/x/graphql_tools@0.0.2/mod.ts";
56 | import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts";
57 | import { readAll } from "https://deno.land/std@0.148.0/streams/conversion.ts";
58 |
59 | import { guarDenoQL } from "https://deno.land/x/guardenoql@v1.0.1/mod.ts";
60 | // update GuarDenoQL import URL with most recent version
61 |
62 | type Request = OpineRequest & { json: () => Promise };
63 |
64 | const typeDefs = gql`
65 | type Query {
66 | hello: String
67 | }
68 | `;
69 |
70 | const resolvers = { Query: { hello: () => `Hello World!` } };
71 | const dec = new TextDecoder();
72 | const schema = makeExecutableSchema({ resolvers, typeDefs });
73 | const app = opine();
74 |
75 | app
76 | .use("/graphql", async (req, res) => {
77 | const request = req as Request;
78 |
79 | request.json = async () => {
80 | const rawBody = await readAll(req.raw);
81 | const body = JSON.parse(dec.decode(rawBody));
82 | const query = body.query;
83 |
84 | const error = guarDenoQL(schema, query, {
85 | depthLimitOptions: {
86 | maxDepth: 4, // maximum depth allowed before a request is rejected
87 | callback: (args) => console.log("query depth is:", args), // optional
88 | },
89 | costLimitOptions: {
90 | maxCost: 5000, // maximum cost allowed before a request is rejected
91 | mutationCost: 5, // cost of a mutation
92 | objectCost: 2, // cost of retrieving an object
93 | scalarCost: 1, // cost of retrieving a scalar
94 | depthCostFactor: 1.5, // multiplicative cost of each depth level
95 | callback: (args) => console.log("query cost is:", args), // optional
96 | },
97 | });
98 |
99 | if (error !== undefined && !error.length) {
100 | return body;
101 | } else {
102 | const errorMessage = { error };
103 | return res.send(JSON.stringify(errorMessage));
104 | }
105 | };
106 |
107 | const resp = await GraphQLHTTP({
108 | schema,
109 | context: (request) => ({ request }),
110 | graphiql: true,
111 | })(request);
112 |
113 | for (const [k, v] of resp.headers.entries()) res.headers?.append(k, v);
114 | res.status = resp.status;
115 | res.send(await resp.text());
116 | })
117 | .listen(3000, () => console.log(`☁ Started on http://localhost:3000`));
118 | ```
119 |
120 | GuarDenoQL is fully customizable.
121 |
122 | Users can use either the depth limiter, cost limiter or both.
123 |
124 | The first argument is the `schema`, the second argument is the `query`, and the
125 | third argument is an `Object` with up to two properties: `depthLimitOptions`
126 | and/or `costLimitOptions`.
127 |
128 | ### **Depth Limit Configuration**
129 |
130 | This feature limits the depth of a document.
131 |
132 | ```typescript
133 | const error = guarDenoQL(schema, query, {
134 | depthLimitOptions: {
135 | maxDepth: 4, // maximum depth allowed before a request is rejected
136 | callback: (args) => console.log("query depth is:", args), // optional
137 | },
138 | });
139 | ```
140 |
141 | The `depthLimitOptions` object has two properties to configure:
142 |
143 | 1. `maxDepth`: the depth limiter will throw a validation error if the document
144 | has a greater depth than the user-supplied `maxDepth`
145 |
146 | 2. optional `callback` function: receives an `Object` that maps the name of the
147 | operation to its corresponding query depth
148 |
149 | ### **Cost Limit Configuration**
150 |
151 | This feature applies a cost analysis algorithm to block queries that are too
152 | expensive.
153 |
154 | ```typescript
155 | const error = guarDenoQL(schema, query, {
156 | costLimitOptions: {
157 | maxCost: 5000, // maximum cost allowed before a request is rejected
158 | mutationCost: 5, // cost of a mutation
159 | objectCost: 2, // cost of retrieving an object
160 | scalarCost: 1, // cost of retrieving a scalar
161 | depthCostFactor: 1.5, // multiplicative cost of each depth level
162 | callback: (args) => console.log("query cost is:", args), // optional
163 | },
164 | });
165 | ```
166 |
167 | The `costLimitOptions` object has six properties to configure:
168 |
169 | 1. `maxCost`: the cost limiter will throw a validation error if the document has
170 | a greater cost than the user-supplied `maxCost`
171 |
172 | 2. `mutationCost`: represents the cost of a mutation (some popular
173 | [cost analysis algorithms](https://shopify.engineering/rate-limiting-graphql-apis-calculating-query-complexity)
174 | make mutations more expensive than queries)
175 |
176 | 3. `objectCost`: represents the cost of an object that has subfields
177 |
178 | 4. `scalarCost`: represents the cost of a scalar
179 |
180 | 5. `depthCostFactor`: the multiplicative cost of each depth level
181 |
182 | 6. optional `callback` function: receives an `Object` that maps the name of the
183 | operation to its corresponding query cost
184 |
185 | # Functionality
186 |
187 | ### **Depth Limiter**
188 |
189 |