├── .dockerignore ├── src ├── vite-env.d.ts ├── assets │ ├── cross.png │ ├── tick.png │ ├── fortifyQL.PNG │ └── FortifyQLDemo4.gif ├── App.tsx ├── main.tsx ├── interfaces │ └── results.ts ├── stylesheets │ ├── ag-theme-custom.scss │ ├── _variables.scss │ └── App.scss ├── components │ ├── ModalAccordion.tsx │ ├── statusIconRenderer.tsx │ ├── Checkbox.tsx │ ├── SecurityDashboard.tsx │ ├── ModalCellRender.tsx │ ├── ScanResultsTable.tsx │ └── ScanConfigForm.tsx ├── __tests__ │ ├── api-backend.js │ ├── misc.js │ └── api-client.js └── utils │ └── format.ts ├── test └── __mocks__ │ ├── fileMock.js │ └── text-encoder.mock.ts ├── babel.config.json ├── Dockerfile.dev ├── tsconfig.prod.json ├── docker-compose.yml ├── tsconfig.node.json ├── .gitignore ├── server ├── functionsAndInputs │ ├── titles.ts │ ├── types.ts │ ├── query.ts │ ├── inputsAndKeywords.ts │ └── generateHelper.ts ├── pentesting │ ├── getSchema.ts │ ├── batching.ts │ ├── verboseError.ts │ ├── injection.ts │ └── circularQuery.ts ├── server.ts └── info.ts ├── vite.config.ts ├── .prettierrc ├── tsconfig.server.json ├── jest.config.ts ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── public └── vite.svg ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /src/assets/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FortifyQL/HEAD/src/assets/cross.png -------------------------------------------------------------------------------- /src/assets/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FortifyQL/HEAD/src/assets/tick.png -------------------------------------------------------------------------------- /src/assets/fortifyQL.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FortifyQL/HEAD/src/assets/fortifyQL.PNG -------------------------------------------------------------------------------- /src/assets/FortifyQLDemo4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FortifyQL/HEAD/src/assets/FortifyQLDemo4.gif -------------------------------------------------------------------------------- /test/__mocks__/text-encoder.mock.ts: -------------------------------------------------------------------------------- 1 | import { TextEncoder } from 'util'; 2 | 3 | global.TextEncoder = TextEncoder; 4 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | EXPOSE 3000 5173 7 | CMD ["npm", "run", "dev"] 8 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "exclude": ["node_modules", "dist", "**/*.spec.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './stylesheets/App.scss'; 2 | import SecurityDashboard from './components/SecurityDashboard'; 3 | 4 | function App() { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile.dev 7 | ports: 8 | - "3000:3000" 9 | - "5173:5173" 10 | environment: 11 | - NODE_ENV=production 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /server/functionsAndInputs/titles.ts: -------------------------------------------------------------------------------- 1 | export const SQLtitles = { 2 | booleanBased: 'Boolean Based SQL Injection', 3 | errorBased: 'Error Based SQL Injection', 4 | timeBased: 'Time-Based Blind SQL Injection', 5 | }; 6 | 7 | export const batchTitles = { 8 | identical: 'Multiple Identical Batching Attack', 9 | exhaustive: 'Resource Exhaustive and Nested Batching Attack', 10 | }; 11 | -------------------------------------------------------------------------------- /src/interfaces/results.ts: -------------------------------------------------------------------------------- 1 | export interface ITestResult { 2 | id: string; 3 | status: string; 4 | title: string; 5 | details: { 6 | query: string[] | string; 7 | response: string; 8 | description: string; 9 | solution?: string; 10 | link?: string; 11 | error?: string; 12 | }; 13 | severity: string; 14 | testDuration: string; 15 | lastDetected: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/stylesheets/ag-theme-custom.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | .ag-row-fail { 4 | background-color: $row-fail-color; 5 | } 6 | 7 | .ag-row-pass { 8 | background-color: $row-pass-color; 9 | } 10 | 11 | .ag-status-icons { 12 | width: 20px; 13 | height: 20px; 14 | } 15 | 16 | #ag-results-table { 17 | padding: 10px; 18 | } 19 | 20 | .ag-header-cell-resize { 21 | z-index: 0; 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: 'http://localhost:3000/', 11 | changeOrigin: true, 12 | secure: false, 13 | }, 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "jsxSingleQuote": true, 8 | "arrowParens": "always", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf", 11 | "overrides": [ 12 | { 13 | "files": "*.json", 14 | "options": { 15 | "printWidth": 200 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ESNext", 6 | "outDir": "./build/server", 7 | "rootDir": "./server", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true 10 | }, 11 | "include": ["server/**/*.ts"], 12 | "ts-node": { 13 | "esm": true, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "transform": { 3 | "^.+\\.(ts|tsx)$": "ts-jest", 4 | "^.+\\.(js|jsx)$": "babel-jest" 5 | }, 6 | "testEnvironment": "jest-environment-jsdom", 7 | "setupFiles": ["/test/__mocks__/text-encoder.mock.ts"], 8 | "moduleNameMapper": { 9 | "\\.(gif|ttf|eot|svg|png)$": "/test/__mocks__/fileMock.js", 10 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 11 | }, 12 | "testPathIgnorePatterns": ["/src/__tests__/mocks/"] 13 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | FortifyQL 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/ModalAccordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Collapse } from 'react-collapse'; 3 | 4 | interface IAccordionProps { 5 | label: string; 6 | defaultIsOpen?: boolean; 7 | children: React.ReactNode; 8 | } 9 | 10 | const ModalAccordion: React.FC = ({ children }) => { 11 | const [isExpanded, setIsExpanded] = useState(false); 12 | 13 | const toggleExpand = () => { 14 | setIsExpanded(!isExpanded); 15 | }; 16 | 17 | return ( 18 |
19 | 22 | {children} 23 |
24 | ); 25 | }; 26 | 27 | export default ModalAccordion; 28 | -------------------------------------------------------------------------------- /src/__tests__/api-backend.js: -------------------------------------------------------------------------------- 1 | // import request from 'supertest'; 2 | // import server from '../../server/server'; 3 | 4 | test('should return empty array when no test are selected', async () => { 5 | // const payload = { 6 | // API: 'https://swapi-graphql.netlify.app/.netlify/functions/index', 7 | // tests: [], 8 | // }; 9 | 10 | // const response = await request(server).post('/api/runpentest').send(payload); 11 | // expect(response.body).toEqual([]); 12 | }); 13 | 14 | test('should return status code 200 when tests are submitted', async () => { 15 | // const payload = { 16 | // API: 'https://example.com/graphql', 17 | // tests: ['Batching', 'Circular'], 18 | // }; 19 | 20 | // const response = await request(server).post('/api/runpentest').send(payload); 21 | // expect(response.statusCode).toBe(200); 22 | }); 23 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2021: true }, 4 | extends: [ 5 | 'airbnb-typescript', 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:react-hooks/recommended', 9 | 'prettier' 10 | ], 11 | ignorePatterns: ['dist', '.eslintrc.cjs'], 12 | parser: '@typescript-eslint/parser', 13 | plugins: ['react-refresh','react','import',"prettier"], 14 | parserOptions: { 15 | project: './tsconfig.json', //can delete later not sure what it does 16 | }, 17 | rules: { 18 | 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], 19 | 'react-refresh/only-export-components': [ 20 | 'warn', 21 | { allowConstantExport: true }, 22 | ], 23 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 24 | "no-loop-func": "off", 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": false /* change to true after development */, 21 | "noUnusedParameters": false /* change to true after development */, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src", "server/**/*"], 25 | "exclude": ["node_modules", "dist", "build"], 26 | "references": [{ "path": "./tsconfig.node.json" }], 27 | "ts-node": { 28 | "esm": true, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/statusIconRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../stylesheets/ag-theme-custom.scss'; 3 | 4 | interface InitParams { 5 | data: { 6 | status: string; 7 | }; 8 | } 9 | 10 | export class StatusIcons { 11 | 12 | eGui: HTMLElement | null = null 13 | init(params: InitParams) { 14 | const element = document.createElement('span'); 15 | const imageElement = document.createElement('img'); 16 | if (params.data.status === 'Fail') { 17 | imageElement.src = '../src/assets/cross.png'; 18 | imageElement.alt = 'red x indicating status failed'; 19 | imageElement.className = 'ag-status-icons'; 20 | } else { 21 | imageElement.src = '../src/assets/tick.png'; 22 | imageElement.alt = 'green check indicating status passed'; 23 | imageElement.className = 'ag-status-icons'; 24 | } 25 | element.appendChild(imageElement); 26 | this.eGui = element; 27 | } 28 | 29 | getGui(): HTMLElement | null { 30 | return this.eGui; 31 | } 32 | } -------------------------------------------------------------------------------- /src/stylesheets/_variables.scss: -------------------------------------------------------------------------------- 1 | /* reset default styles */ 2 | 3 | //Sass variables 4 | 5 | // Other colors 6 | $red: rgb(232, 28, 38); 7 | $white: rgb(255, 255, 255); 8 | $black: rgb(0, 0, 0); 9 | $purple: rgba(112, 8, 128, 0.5); 10 | 11 | // AG Colors 12 | $green: rgb(143, 217, 168); 13 | $red: rgb(232, 28, 38); 14 | 15 | $row-fail-color: rgba($red, 0.1); 16 | $row-pass-color: rgba($green, 0.2); 17 | 18 | // Color Palette 19 | $primary-color: rgb(75, 119, 141); 20 | $secondary-color: rgb(40, 181, 181); 21 | $tertiary-color: rgb(143, 217, 168); 22 | $quatenary-color: rgb(210, 230, 156); 23 | $seventy-color: rgba($primary-color, 0.7); 24 | $fifty-color: rgba($primary-color, 0.5); 25 | $thirty-color: rgba($primary-color, 0.3); 26 | $ten-color: rgba($primary-color, 0.1); 27 | $five-color: rgba($primary-color, 0.05); 28 | 29 | // Colors for specific elements 30 | $background-color: rgba(208, 204, 204, 0.441); 31 | $slider-color: rgb(173, 172, 172); 32 | $slider-color-selected: rgb(65, 105, 225, 0.42); 33 | // $slider-color-selected: $green; 34 | $border-color: rgb(121, 118, 118); 35 | -------------------------------------------------------------------------------- /server/functionsAndInputs/types.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | 3 | export type PentestType = { 4 | generateQueries: RequestHandler; 5 | attack: RequestHandler; 6 | }; 7 | export interface GraphQLType { 8 | name: string; 9 | kind: string; 10 | fields?: GraphQLField[]; 11 | } 12 | export interface GraphQLField { 13 | name: string; 14 | args?: GraphQLArgs[]; 15 | type: GraphQLTypeReference; 16 | fields?: GraphQLField; 17 | } 18 | export interface GraphQLArgs { 19 | name: string; 20 | type?: GraphQLTypeReference; 21 | } 22 | export interface GraphQLTypeReference { 23 | kind: string; 24 | name?: string; 25 | ofType?: GraphQLTypeReference; 26 | fields?: GraphQLField[]; 27 | } 28 | export interface QueryResult { 29 | id: string; 30 | status: string; 31 | title: string; 32 | details: { 33 | error?: string; 34 | query: string; 35 | response: string; 36 | description: string; 37 | solution: string; 38 | link: string; 39 | } 40 | severity: string | number; 41 | testDuration: string | number; 42 | lastDetected: string | number; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Checkbox.tsx 5 | * @author MADR Productions - MK 6 | * @date 01-13-24 7 | * @description This file defines a React functional component called Checkbox, which represents a checkbox element with associated properties like value, label, and description. 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | 13 | interface ICheckboxProps { 14 | value: string; 15 | label: string; 16 | description: string; 17 | isChecked: boolean; 18 | onChange: (value: string, isChecked: boolean) => void; 19 | } 20 | 21 | const Checkbox: React.FC = (props) => { 22 | 23 | const { value, label, description, isChecked, onChange } = props; 24 | 25 | const handleCheckboxChange = () => { 26 | onChange(value, !isChecked); 27 | } 28 | 29 | return ( 30 |
31 | 35 | 38 |
39 | ); 40 | }; 41 | 42 | export default Checkbox; -------------------------------------------------------------------------------- /server/functionsAndInputs/query.ts: -------------------------------------------------------------------------------- 1 | import { QueryResult } from './types'; 2 | export function createQueryResult( 3 | category: string, 4 | query: string, 5 | ID: number, 6 | ): QueryResult { 7 | return { 8 | id: `${category}-${ID}`, 9 | status: 'Pass', 10 | title: '', 11 | details: { 12 | query: query, 13 | response: '', 14 | description: '', 15 | solution: '', 16 | link: '', 17 | }, 18 | severity: 'P1', 19 | testDuration: '', 20 | lastDetected: `${new Date().toLocaleTimeString('en-GB')} - ${new Date() 21 | .toLocaleDateString('en-GB') 22 | .split('/') 23 | .reverse() 24 | .join('-')}`, 25 | }; 26 | } 27 | export function createErrorResult( 28 | category: string, 29 | query: string, 30 | ID: number, 31 | ): QueryResult { 32 | return { 33 | id: `${category}-${ID}`, 34 | status: 'Error', 35 | title: 'Failed to Run Test', 36 | details: { 37 | query: '', 38 | response: '', 39 | description: '', 40 | solution: '', 41 | link: '', 42 | }, 43 | severity: 'P1', 44 | testDuration: '', 45 | lastDetected: `${new Date().toLocaleTimeString('en-GB')} - ${new Date() 46 | .toLocaleDateString('en-GB') 47 | .split('/') 48 | .reverse() 49 | .join('-')}`, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /server/functionsAndInputs/inputsAndKeywords.ts: -------------------------------------------------------------------------------- 1 | export const SQLInputs = [ 2 | // Boolean Based SQL Injection 3 | "' OR 1 = 1; --", 4 | "1' OR '1' = '1 /*", 5 | "admin' OR '1'='1'--", 6 | 7 | // Error Based SQL Injection 8 | "'", 9 | ';', 10 | '--', 11 | 12 | // Time-Based Blind SQL Injection 13 | '; SELECT SLEEP(15) --', // MySQL, MariaDB 14 | '; SELECT pg_sleep(15); --', // PostgreSQL 15 | "; IF (1=1) WAITFOR DELAY '00:00:15'--", // Another example for MySQL 16 | ]; 17 | export const sqlErrorKeywords = [ 18 | 'syntax error', 19 | 'unexpected', 20 | 'mysql_fetch', 21 | 'invalid query', 22 | ]; 23 | export const batchingErrorKeywords: string[] = [ 24 | 'too many operations', 25 | 'batch size exceeds', 26 | 'operation limit', 27 | 'query complexity exceeds', 28 | 'rate limit exceeded', 29 | 'throttle', 30 | 'unauthorized batch request', 31 | 'unexpected token', 32 | 'batching not supported', 33 | 'anonymous operation', 34 | 'must be the only defined operation', 35 | 'batch', 36 | 'rate limit', 37 | 'server error', 38 | 'api limit exceeded', 39 | ]; 40 | export const verboseErrorKeywords: string[] = [ 41 | 'did you mean ', 42 | 'syntax error graphql', 43 | 'expected', 44 | 'found', 45 | 'is required', 46 | 'not provided', 47 | 'argument', 48 | 'sub selection', 49 | ]; 50 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__tests__/misc.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom'; 3 | import { render, screen, waitFor } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import SecurityDashboard from '../components/SecurityDashboard'; 6 | 7 | jest.mock('react-modal', () => ({ 8 | ...jest.requireActual('react-modal'), 9 | setAppElement: () => {}, 10 | })); 11 | 12 | global.fetch = jest.fn(() => 13 | Promise.resolve({ 14 | ok: true, 15 | json: () => Promise.resolve([]), 16 | }), 17 | ); 18 | 19 | beforeEach(() => { 20 | fetch.mockClear(); 21 | }); 22 | 23 | test('Dashboard initally renders config form and not result or loading screen', () => { 24 | render(); 25 | const configForm = screen.getByPlaceholderText('Enter GraphQL API URI Here'); 26 | const loader = screen.queryByTestId('table-loader'); 27 | 28 | expect(configForm).toBeInTheDocument(); 29 | expect(loader).toBeNull(); 30 | }); 31 | 32 | test('Results table shown after loading screen when a submission is made', async () => { 33 | render(); 34 | const url = 'www.fakeurl.com'; 35 | 36 | const urlInput = screen.getByPlaceholderText('Enter GraphQL API URI Here'); 37 | await userEvent.type(urlInput, url); 38 | const scanButton = screen.getByRole('button', { name: 'Scan' }); 39 | await userEvent.click(scanButton); 40 | 41 | await waitFor(() => expect(screen.queryByText(/Scanning.../i)).not.toBeInTheDocument()); 42 | expect(screen.getByText(/Security Scan Results/i)).toBeInTheDocument() 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/api-client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import ScanConfigForm from '../components/ScanConfigForm'; 5 | 6 | test('Configuration form sends a request to the correct endpoint', async () => { 7 | const onScanSubmit = jest.fn(); 8 | render(); 9 | 10 | const url = 'www.fakeurl.com'; 11 | 12 | const urlInput = screen.getByPlaceholderText('Enter GraphQL API URI Here'); 13 | await userEvent.type(urlInput, url); 14 | 15 | const scanButton = screen.getByRole('button', { name: 'Scan' }); 16 | await userEvent.click(scanButton); 17 | 18 | expect(onScanSubmit).toHaveBeenCalledTimes(1); 19 | expect(onScanSubmit).toHaveBeenCalledWith(url, []); 20 | }); 21 | 22 | test('If no url is provided, prompts user to fill the field and does not send request', async () => { 23 | const onScanSubmit = jest.fn(); 24 | render(); 25 | 26 | const selectAllButton = screen.getByRole('button', { name: 'Select All' }); 27 | await userEvent.click(selectAllButton); 28 | 29 | const scanButton = screen.getByRole('button', { name: 'Scan' }); 30 | await userEvent.click(scanButton); 31 | 32 | expect(onScanSubmit).not.toHaveBeenCalled(); 33 | }); 34 | 35 | test('Select All buttons will properly check all test options', async () => { 36 | const onScanSubmit = jest.fn(); 37 | render(); 38 | 39 | const url = 'www.fakeurl.com'; 40 | 41 | const urlInput = screen.getByPlaceholderText('Enter GraphQL API URI Here'); 42 | await userEvent.type(urlInput, url); 43 | 44 | const selectAllButton = screen.getByRole('button', { name: 'Select All' }); 45 | await userEvent.click(selectAllButton); 46 | 47 | const scanButton = screen.getByRole('button', { name: 'Scan' }); 48 | await userEvent.click(scanButton); 49 | 50 | expect(onScanSubmit).toHaveBeenCalledTimes(1); 51 | expect(onScanSubmit).toHaveBeenCalledWith(url, [ 52 | 'Batching', 53 | 'Circular', 54 | 'SQL', 55 | 'Verbose', 56 | ]); 57 | }); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fortifyql", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev:frontend": "vite --host --port 5173", 8 | "dev:backend": "nodemon --exec node --loader ts-node/esm server/server.ts", 9 | "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"", 10 | "build:frontend": "vite build", 11 | "build:backend": "tsc -p tsconfig.prod.json", 12 | "build": "npm run build:backend && npm run build:frontend", 13 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 14 | "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx,json}'", 15 | "preview": "vite preview", 16 | "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", 17 | "test": "jest" 18 | }, 19 | "dependencies": { 20 | "ag-grid-community": "^30.1.0", 21 | "ag-grid-react": "^30.1.0", 22 | "cors": "^2.8.5", 23 | "eslint-config-airbnb-typescript": "^17.1.0", 24 | "eslint-plugin": "^1.0.1", 25 | "express": "^4.18.2", 26 | "fs": "^0.0.1-security", 27 | "node-fetch": "^3.3.2", 28 | "path": "^0.12.7", 29 | "react": "^18.2.0", 30 | "react-collapse": "^5.1.1", 31 | "react-dom": "^18.2.0", 32 | "react-modal": "^3.16.1", 33 | "react-spinners": "^0.13.8", 34 | "url": "^0.11.3" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.23.9", 38 | "@babel/preset-env": "^7.23.9", 39 | "@babel/preset-react": "^7.23.3", 40 | "@testing-library/jest-dom": "^6.4.2", 41 | "@testing-library/react": "^14.2.1", 42 | "@testing-library/user-event": "^14.5.2", 43 | "@types/cors": "^2.8.14", 44 | "@types/express": "^4.17.17", 45 | "@types/jest": "^29.5.12", 46 | "@types/node": "^20.6.3", 47 | "@types/react": "^18.2.15", 48 | "@types/react-collapse": "^5.0.4", 49 | "@types/react-dom": "^18.2.7", 50 | "@types/react-modal": "^3.16.1", 51 | "@typescript-eslint/eslint-plugin": "^6.0.0", 52 | "@typescript-eslint/parser": "^6.0.0", 53 | "@vitejs/plugin-react": "^4.0.3", 54 | "babel-jest": "^29.7.0", 55 | "concurrently": "^8.2.1", 56 | "eslint": "^8.45.0", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-plugin-import": "^2.28.1", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "eslint-plugin-react": "^7.33.2", 61 | "eslint-plugin-react-hooks": "^4.6.0", 62 | "eslint-plugin-react-refresh": "^0.4.3", 63 | "identity-obj-proxy": "^3.0.0", 64 | "jest": "^29.7.0", 65 | "jest-environment-jsdom": "^29.7.0", 66 | "jest-environment-jsdom-global": "^4.0.0", 67 | "msw": "^2.1.7", 68 | "nodemon": "^3.0.1", 69 | "prettier": "^3.0.3", 70 | "sass": "^1.69.1", 71 | "supertest": "^6.3.4", 72 | "ts-jest": "^29.1.2", 73 | "ts-node": "^10.9.1", 74 | "typescript": "^5.2.2", 75 | "vite": "^4.4.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/SecurityDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ScanConfigForm from './ScanConfigForm'; 3 | import ScanResultsTable from './ScanResultsTable'; 4 | import { ITestResult } from '../interfaces/results'; 5 | import ClipLoader from 'react-spinners/ClipLoader'; 6 | 7 | const SecurityDashboard: React.FC = () => { 8 | const [scanResults, setScanResults] = useState([]); 9 | // Test Result Table loading state 10 | const [loading, setLoading] = useState(false); 11 | // Form visibility state 12 | const [showConfigForm, setShowConfigForm] = useState(true); 13 | 14 | const handleScanSubmit = async ( 15 | endpoint: string, 16 | selectedTests: string[], 17 | ) => { 18 | setLoading(true); 19 | 20 | try { 21 | setShowConfigForm(false); // Hide the config form after submitting 22 | 23 | const requestBody = JSON.stringify({ 24 | API: endpoint, 25 | tests: selectedTests, 26 | }); 27 | 28 | const response = await fetch('/api/runpentest', { 29 | method: 'POST', 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | body: requestBody, 34 | }); 35 | 36 | if (!response.ok) { 37 | throw new Error('Test api response error occurred'); 38 | } 39 | 40 | const data = await response.json(); 41 | 42 | setScanResults(data); // Update scanResults with the received data 43 | } catch (error) { 44 | console.error('Error fetching data:', error); 45 | } finally { 46 | setLoading(false); 47 | } 48 | }; 49 | 50 | const handleDisplayTestConfig = () => { 51 | setShowConfigForm(true); 52 | }; 53 | 54 | return ( 55 |
56 |
57 | FortifyQL logo of an abstract red maze in the shape of a shield with a keyhole at the center 62 |

63 | FortifyQL 64 |

65 |
66 | {showConfigForm ? ( 67 | 68 | ) : ( 69 |
70 | {loading ? ( 71 |
72 |

Scanning...

73 | 80 |
81 | ) : ( 82 | 86 | )} 87 |
88 | )} 89 |
90 | ); 91 | }; 92 | 93 | export default SecurityDashboard; 94 | -------------------------------------------------------------------------------- /server/pentesting/getSchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module getSchema.ts 5 | * @author MADR Productions - AY 6 | * @date 9-23-23 7 | * @description middleware for all tests to generate introspection query to acquire schema for generating other queries. 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import { Request, Response } from 'express'; 13 | 14 | const getSchema = async (req: Request, res: Response) => { 15 | const fetchModule = await import('node-fetch'); 16 | const fetch = fetchModule.default; 17 | 18 | const query = `query IntrospectionQuery { 19 | __schema { 20 | queryType { 21 | name 22 | } 23 | mutationType { 24 | name 25 | } 26 | subscriptionType { 27 | name 28 | } 29 | types { 30 | ...FullType 31 | } 32 | directives { 33 | name 34 | description 35 | args { 36 | ...InputValue 37 | } 38 | } 39 | } 40 | } 41 | 42 | fragment FullType on __Type { 43 | kind 44 | name 45 | description 46 | fields(includeDeprecated: true) { 47 | name 48 | description 49 | args { 50 | ...InputValue 51 | } 52 | type { 53 | ...TypeRef 54 | } 55 | isDeprecated 56 | deprecationReason 57 | } 58 | inputFields { 59 | ...InputValue 60 | } 61 | interfaces { 62 | ...TypeRef 63 | } 64 | enumValues(includeDeprecated: true) { 65 | name 66 | description 67 | isDeprecated 68 | deprecationReason 69 | } 70 | possibleTypes { 71 | ...TypeRef 72 | } 73 | } 74 | 75 | fragment InputValue on __InputValue { 76 | name 77 | description 78 | type { 79 | ...TypeRef 80 | } 81 | defaultValue 82 | } 83 | 84 | fragment TypeRef on __Type { 85 | kind 86 | name 87 | ofType { 88 | kind 89 | name 90 | ofType { 91 | kind 92 | name 93 | ofType { 94 | kind 95 | name 96 | } 97 | } 98 | } 99 | }`; 100 | try { 101 | console.log('Executing Introspection Query...'); 102 | const API = req.body.API; 103 | const response = await fetch(API, { 104 | method: 'POST', 105 | headers: { 106 | 'Content-Type': 'application/graphql', 107 | }, 108 | body: query, 109 | }); 110 | 111 | const result: any = await response.json(); 112 | res.locals.schema = result; 113 | console.log('Retrieved Schema...'); 114 | return; 115 | } catch (err) { 116 | console.log('getSchema middleware', err); 117 | res 118 | .status(400) 119 | .json('Unable to retrieve schema, please turn introspection on '); 120 | } 121 | }; 122 | 123 | export default getSchema; 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FortifyQL is a sophisticated security scanning tool designed to provide developers with deep insights into potential security vulnerabilities within their GraphQL API's. FortifyQL dynamically generates queries that emulate common attacks identified by the Open Web Application Security Project (OWASP). The responses are analyzed for insecure default configurations and missing security measures that may lead to Injection, Denial of Service (DOS), and Batching attacks. The data is displayed within an intuitive and user-friendly interface, making it accessible to developers of all skill levels. 2 | 3 | Let's further explore FortifyQL's initial robust test suites. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Features](##Features) 8 | 2. [Setup](##Setup) 9 | 3. [Roadmap](##Roadmap) 10 | 4. [Contributors](##Contributors) 11 | 12 | ## Features: 13 | 14 | • SQL Injection (Boolean, Error, and Time-based): These tests check for vulnerabilities in different types of database systems, using various injection techniques to expose weaknesses. 15 | 16 | • Denial of Service (DoS: Circular): This test simulates a circular DoS-type attack, rendering a system unresponsive through excessive resource consumption. 17 | 18 | • Batching (Multiple Queries and Resource Exhaustive/Nested): Batching and nesting tests aim to uncover resource exhaustion and complex operations that could harm your server. 19 | 20 | • Verbose Error: This test identifies security misconfigurations by exposing excessive system information. 21 | 22 | ![](/src/assets/FortifyQLDemo4.gif) 23 | 24 | ## Setup: 25 | 26 | 1. Clone the FortifyQL repository locally and install required modules: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 2. Launch FortifyQL from the command line: 32 | -`npm run dev`: Runs frontend and backend development servers concurrently for an integrated development experience. 33 | 34 | 3. Enable introspection for GraphQL endpoint. 35 | 36 | 4. Navigate to http://localhost:5173/ to view web application 37 | 38 | 5. Input GraphQL URI into input field 39 | 40 | 6. Toggle desired tests to execute 41 | 42 | 43 | ## Roadmap 44 | | Feature | Status | 45 | | ------------------------------------------------ | ------ | 46 | | Dynamic Query Generation | ✅ | 47 | | In-Depth Reporting | ✅ | 48 | | Increase Testing Coverage | ⏳ | 49 | | Expansion of Pentesting Suite | ⏳ | 50 | | Introspection and URI Authentication | ⏳ | 51 | | Node and Rate Limit Calculator | ⚡️ | 52 | 53 | 54 | - ✅ = Completed 55 | - ⏳ = In-Progress 56 | - ⚡️ = Backlog 57 | 58 | ## Contributors 59 | 60 | • Rachel Power: [LinkedIn](https://www.linkedin.com/in/rachel-b-power/) | [GitHub](https://github.com/rpower15) 61 | 62 | • Ayden Yip: [LinkedIn](https://www.linkedin.com/in/aydenyip/) | [GitHub](https://github.com/aydenyipcs) 63 | 64 | • Megan Kabat: [LinkedIn](https://www.linkedin.com/in/megan-kabat/) | [GitHub](https://github.com/mnkabat) 65 | 66 | • David Yoon: [LinkedIn](https://www.linkedin.com/in/davidlyoon/) | [GitHub](https://github.com/DYoonity) 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/components/ModalCellRender.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import { ITestResult } from '../interfaces/results'; 4 | import { prettifyJson, prettyPrintGraphQL } from '../utils/format'; 5 | import ModalAccordion from './ModalAccordion'; 6 | 7 | interface IModalCellRendererProps { 8 | isModalOpen: boolean; 9 | setIsModalOpen: React.Dispatch>; 10 | closeModal: () => void; 11 | data: ITestResult; 12 | } 13 | 14 | Modal.setAppElement('#root'); 15 | 16 | const customStyles = { 17 | content: { 18 | backgroundColor: 'rgba(208, 204, 204, 0)', 19 | top: '5%', 20 | left: 'auto', 21 | right: 'auto', 22 | bottom: 'auto', 23 | border: 0, 24 | }, 25 | }; 26 | 27 | const ModalCellRenderer: React.FC = ({ data }) => { 28 | const [isModalOpen, setIsModalOpen] = useState(false); 29 | const [modalData, setModalData] = useState(data); 30 | const [isError, setError] = useState(false); 31 | 32 | useEffect(() => { 33 | setModalData(data); 34 | if (data.details.error) setError(true); 35 | }, [data]); 36 | 37 | const openModal = () => { 38 | setIsModalOpen(true); 39 | }; 40 | 41 | const closeModal = () => { 42 | setIsModalOpen(false); 43 | }; 44 | 45 | return ( 46 |
47 | 48 | {isModalOpen && ( 49 | 54 |
55 | 56 | 59 | 60 | 63 | 64 |
65 |

Description:

66 |

{modalData.details.description}

67 |

Query:

68 | 69 |
 70 |                   
 71 |                     {prettyPrintGraphQL(modalData.id, modalData.details.query)}
 72 |                   
 73 |                 
74 |
75 |

Response:

76 | 77 |
 78 |                   {prettifyJson(modalData.details.response)}
 79 |                 
80 |
81 | {isError && ( 82 |
83 |

Issue Detected:

84 |

{data.details.error}

85 |
86 | )} 87 |

Solution:

88 |

{modalData.details.solution}

89 | 90 | {modalData.details.link}{' '} 91 | 92 |
93 |
94 |
95 | )} 96 |
97 | ); 98 | }; 99 | 100 | export default ModalCellRenderer; 101 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module server.ts 5 | * @author MADR Productions - AY 6 | * @date 9-21-23 7 | * @description server for FortifyQL 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import express, { Request, Response, NextFunction } from 'express'; 13 | // import path from 'path' 14 | import cors from 'cors'; 15 | 16 | const server = express(); 17 | const PORT = 3000; 18 | 19 | // REQUIRED ROUTES && MIDDLEWARE 20 | import getSchema from './pentesting/getSchema.ts'; 21 | import { injection } from './pentesting/injection.ts'; 22 | import { batching } from './pentesting/batching.ts'; 23 | import { verboseError } from './pentesting/verboseError.ts'; 24 | import { circularQuery } from './pentesting/circularQuery.ts'; 25 | import { ITestResult } from '../src/interfaces/results.ts'; 26 | 27 | // Use cors 28 | server.use(cors()); 29 | 30 | // PARSE JSON 31 | server.use(express.urlencoded({ extended: true })); 32 | server.use(express.json()); 33 | 34 | //GLOBAL ROUTE CHECK 35 | server.use((req, _res, next) => { 36 | console.log('Request received', req.method, req.path, req.body); 37 | return next(); 38 | }); 39 | //PATHS 40 | server.use('/api/runpentest', async (req: Request, res: Response) => { 41 | try { 42 | console.log('Starting Penetration Testing...'); 43 | await getSchema(req, res); 44 | 45 | const testsMap: { 46 | [key: string]: { generate: Function; evaluate: Function }; 47 | } = { 48 | SQL: { 49 | generate: injection.generateQueries, 50 | evaluate: injection.attack, 51 | }, 52 | Verbose: { 53 | generate: verboseError.generateQueries, 54 | evaluate: verboseError.attack, 55 | }, 56 | Circular: { 57 | generate: circularQuery.generateQueries, 58 | evaluate: circularQuery.attack, 59 | }, 60 | Batching: { 61 | generate: batching.generateQueries, 62 | evaluate: batching.attack, 63 | }, 64 | }; 65 | 66 | const results: ITestResult[] = []; 67 | 68 | const runTest = async (test: string) => { 69 | if (req.body.tests.includes(test)) { 70 | await testsMap[test].generate(req, res); 71 | const testResult = await testsMap[test].evaluate(req, res); 72 | results.push(...testResult); 73 | } 74 | }; 75 | 76 | const runAllTests = async () => { 77 | console.log('running all tests'); 78 | await Promise.all(Object.keys(testsMap).map(runTest)); 79 | }; 80 | 81 | await runAllTests(); 82 | console.log('sending response'); 83 | return res.status(200).json(results); 84 | } catch (err) { 85 | return res.status(400).json('error running tests'); 86 | } 87 | }); 88 | 89 | // GLOBAL ERROR HANDLER 90 | interface CustomError { 91 | log?: string; 92 | status?: number; 93 | message?: { 94 | err: string; 95 | }; 96 | } 97 | 98 | server.use( 99 | (err: CustomError, _req: Request, res: Response, _next: NextFunction) => { 100 | const defaultErr = { 101 | log: 'Express error handler caught unknown middleware error', 102 | status: 500, 103 | message: { err: 'An error occurred' }, 104 | }; 105 | 106 | const errorObj = { ...defaultErr, ...err }; 107 | console.log(errorObj.log); 108 | return res.status(errorObj.status).json(errorObj.message); 109 | }, 110 | ); 111 | 112 | server.listen(PORT, () => console.log(`Listening on port ${PORT}`)); 113 | 114 | export default server; -------------------------------------------------------------------------------- /src/components/ScanResultsTable.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from 'react'; 2 | import { AgGridReact } from 'ag-grid-react'; 3 | import { ColDef, GridApi, RowClassParams } from 'ag-grid-community'; 4 | import 'ag-grid-community/styles/ag-grid.css'; 5 | import 'ag-grid-community/styles/ag-theme-alpine.css'; 6 | import '../stylesheets/ag-theme-custom.scss'; 7 | import ModalCellRenderer from './ModalCellRender'; 8 | import { ITestResult } from '../interfaces/results'; 9 | import { StatusIcons } from './statusIconRenderer'; 10 | 11 | interface IResultsTableProps { 12 | resultsData: ITestResult[]; 13 | handleDisplayTestConfig: () => void; 14 | } 15 | 16 | const ScanResultsTable: React.FC = ({ 17 | resultsData, 18 | handleDisplayTestConfig, 19 | }) => { 20 | // State to store the AG Grid API 21 | const [gridApi, setGridApi] = useState(null); 22 | 23 | const gridStyle = useMemo(() => ({ height: '600px', width: '100%' }), []); 24 | 25 | const colDefs: ColDef[] = [ 26 | { 27 | field: 'status', 28 | maxWidth: 120, 29 | cellRenderer: StatusIcons, 30 | }, 31 | { 32 | headerName: 'Test ID', 33 | field: 'id', 34 | maxWidth: 120, 35 | }, 36 | { field: 'title', minWidth: 250, maxWidth: 400 }, 37 | { 38 | field: 'details', 39 | cellRenderer: 'modalCellRenderer', 40 | maxWidth: 100, 41 | }, 42 | // { field: 'severity', editable: true, maxWidth: 120 }, 43 | { field: 'testDuration', maxWidth: 170 }, 44 | { field: 'lastDetected' }, 45 | ]; 46 | 47 | // AG Grid Column Definitions 48 | const defaultColDef = useMemo( 49 | () => ({ 50 | flex: 1, 51 | filter: true, 52 | sortable: true, 53 | resizable: true, 54 | }), 55 | [], 56 | ); 57 | 58 | // AG Grid assign classes to rows based on pass/fail to color code 59 | const getRowClass = (params: RowClassParams) => { 60 | if (params.data.status === 'Fail') { 61 | return 'ag-row-fail'; 62 | } else { 63 | return 'ag-row-pass'; 64 | } 65 | }; 66 | 67 | // AG Grid custom React components to display in table 68 | const components = useMemo( 69 | () => ({ 70 | modalCellRenderer: ModalCellRenderer, 71 | }), 72 | [], 73 | ); 74 | 75 | // Handles exporting data to CSV 76 | const handleExportCSV = () => { 77 | if (gridApi) { 78 | const params = { 79 | fileName: 'test-results.csv', 80 | columnSeparator: ',', 81 | }; 82 | gridApi.exportDataAsCsv(params); 83 | } 84 | }; 85 | 86 | return ( 87 |
88 |

Security Scan Results

89 |
90 |
91 | 94 | 97 |
98 |
99 | setGridApi(params.api)} 106 | components={components} 107 | /> 108 |
109 |
110 | ); 111 | }; 112 | 113 | export default ScanResultsTable; 114 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prettify a JSON object by converting it to a pretty-printed JSON string 3 | * @param json - JSON object to be prettified 4 | * @returns A string containing the pretty-printed JSON representation 5 | */ 6 | export const prettifyJson = (json: string): string => { 7 | return JSON.stringify(json, null, 2); 8 | }; 9 | 10 | // 11 | /** 12 | * 13 | * Pretty-print GraphQL batching queries by adding line breaks and indentation. 14 | * @param queries - An array of input GraphQL query strings to be formatted. 15 | * @returns A string containing the pretty-printed GraphQL queries. 16 | */ 17 | export const prettyPrintGraphQLBatch = (queries: string[]): string => { 18 | // Indentation string (two spaces) 19 | const indent = ' '; 20 | // Initialize the indentation level 21 | let level = 0; 22 | // Initialize the formatted query string 23 | let prettyQuery = ''; 24 | // Function to add the current indentation 25 | function addIndent() { 26 | prettyQuery += `${indent.repeat(level)}`; 27 | } 28 | // Loop through each query in the array 29 | queries.forEach((query) => { 30 | // Loop through each character in the query 31 | for (let i = 0; i < query.length; i++) { 32 | const char = query[i]; 33 | // When an opening curly brace is encountered, add a line break, increase the indentation level, and add the opening brace 34 | if (char === '{') { 35 | addIndent(); 36 | prettyQuery += `{\n`; 37 | level++; 38 | } 39 | // When a closing curly brace is encountered, decrease the indentation level, add a line break, and then close the brace 40 | else if (char === '}') { 41 | level--; 42 | prettyQuery += `\n`; 43 | addIndent(); 44 | prettyQuery += `}`; 45 | } 46 | // For all other characters, simply append them to the formatted query 47 | else { 48 | prettyQuery += char; 49 | } 50 | } 51 | // Add a separator line between queries in the batch 52 | prettyQuery += '\n\n'; 53 | }); 54 | // Remove trailing newlines 55 | prettyQuery = prettyQuery.trimEnd(); 56 | // Return the formatted query 57 | return prettyQuery; 58 | }; 59 | 60 | /** 61 | * Function to pretty-print a GraphQL query by adding line breaks and indentation. 62 | * @param queryId - Test ID that contains test type to determine what type of pretty print to format 63 | * @param query - The input GraphQL query string to be formatted. 64 | * @returns A string containing the pretty-printed GraphQL query. 65 | */ 66 | 67 | export const prettyPrintGraphQL = ( 68 | queryId: string, 69 | query: string[] | string, 70 | ): string => { 71 | const queryArray = typeof query === 'string' ? [query] : query; 72 | if (queryId.toLowerCase().includes('batch')) { 73 | return prettyPrintGraphQLBatch(queryArray); 74 | } 75 | 76 | // Indentation string (two spaces) 77 | const indent = ' '; 78 | 79 | // Initialize the indentation level 80 | let level = 0; 81 | 82 | // Initialize the formatted query string 83 | let prettyQuery = ''; 84 | 85 | // Loop through each character in the input query 86 | for (let i = 0; i < query.length; i++) { 87 | const char = query[i]; 88 | 89 | // When an opening curly brace is encountered, add a line break and increase the indentation level 90 | if (char === '{') { 91 | prettyQuery += `{\n${indent.repeat(++level)}`; 92 | } 93 | // When a closing curly brace is encountered, add a line break and decrease the indentation level, then close the brace 94 | else if (char === '}') { 95 | prettyQuery += `\n${indent.repeat(--level)}}`; 96 | } 97 | // For all other characters, simply append them to the formatted query 98 | else { 99 | prettyQuery += char; 100 | } 101 | } 102 | 103 | // Return the formatted query 104 | return prettyQuery; 105 | }; 106 | -------------------------------------------------------------------------------- /src/components/ScanConfigForm.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module ScanConfigForm.tsx 5 | * @author MADR Productions - MK 6 | * @date 01-13-24 7 | * @description This component represents a form for configuring a security scan. It includes input fields for a GraphQL API URI, checkboxes for different scan types, and buttons to submit the form and select all available scan types. The component manages the state of selected tests and the URI textbox. 8 | * 9 | * ************************************ 10 | */ 11 | import React, { useState, FormEvent } from 'react'; 12 | import Checkbox from './Checkbox'; 13 | 14 | interface IConfigFormProps { 15 | onScanSubmit: (endpoint: string, tests: string[]) => void; 16 | } 17 | 18 | const ScanConfigForm: React.FC = (props) => { 19 | const { onScanSubmit } = props; 20 | 21 | // Sets the state of textbox input 22 | const [endpoint, setEndpoint] = useState(''); 23 | 24 | // Sets the state of the selected tests array 25 | const [selectedTests, setSelectedTests] = useState([]); 26 | 27 | // Function that manages the textbox state 28 | const handleEndpoint = (e: React.BaseSyntheticEvent) => { 29 | setEndpoint(e.target.value); 30 | }; 31 | 32 | // Function that manages the checkbox state 33 | const handleCheckboxChange = (testType: string, isChecked: boolean) => { 34 | setSelectedTests((prevSelectedTests) => { 35 | return isChecked 36 | ? [...prevSelectedTests, testType] 37 | : prevSelectedTests.filter((test) => test !== testType); 38 | }); 39 | }; 40 | 41 | // Button that submits the form data 42 | const handleSubmit = (e: FormEvent) => { 43 | e.preventDefault(); 44 | onScanSubmit(endpoint, selectedTests); 45 | }; 46 | 47 | // Button that updates the state of selectedTests to include all tests 48 | const selectAllTests = (e: React.BaseSyntheticEvent) => { 49 | e.preventDefault(); 50 | const allTests = ['Batching', 'Circular', 'SQL', 'Verbose']; 51 | setSelectedTests(allTests); 52 | }; 53 | 54 | return ( 55 |
56 |
57 |

Security Scan Configuration

58 |
59 | 65 | 66 | 67 | 73 | 74 | 78 | 79 | 83 | 84 | 89 | 90 | 92 | 93 | 94 |
95 | ); 96 | }; 97 | 98 | export default ScanConfigForm; -------------------------------------------------------------------------------- /server/info.ts: -------------------------------------------------------------------------------- 1 | export const info = { 2 | "Boolean Based SQL Injection": "A technique where attackers manipulate an application by sending SQL queries that produce true or false outcomes.By observing the application's behavior to these binary results, they can deduce valuable information about the underlying database.", 3 | "BBSQLI Solution": "Utilize parameterized queries or prepared statements to ensure data is securely handled. By doing this, you ensure that user input is always treated as data and not executable code. Regularly review and update your database permissions to limit unnecessary access.", 4 | "Error Based SQL Injection": "Attackers intentionally send erroneous SQL queries to provoke the application into revealing database error messages. These messages can inadvertently expose details about the database structure or other sensitive information.", 5 | "EBSQLI Solution": "Turn off detailed error messages for your production environments and use custom error handling to ensure system-specific details are never exposed to the user. Always use parameterized queries to separate user input from SQL logic.", 6 | "Time-Based Blind SQL Injection": "In this subtle attack, perpetrators submit specific SQL queries and then measure the response time of the application. Extended delays can hint at the validity of the attacker's query, allowing them to piece together database insights over time.", 7 | "TMSQLI Solution": "Along with using parameterized queries, implement input validation using a strict whitelist. Monitor and alert on unusual patterns in database response times which could indicate exploitation attempts.", 8 | "Multiple Identical Batching Attack": "By repeatedly sending the same or similar queries in bulk, attackers aim to either uncover vulnerabilities in the application's handling of batched requests or create disruptions in its operations.", 9 | "MIBA Solution": "Implement rate-limiting to detect and block repeated identical queries from a single source. Utilize web application firewalls (WAFs) to monitor and filter out suspicious patterns in incoming traffic.", 10 | "Resource Exhaustive and Nested Batching Attack": "Attackers craft intricate and nested SQL queries, then send them in large batches. The goal is to overburden the database or application resources, potentially leading to failures or unintentional exposure of information.", 11 | "REANBA Solution": "Analyze and set limits on query depth and complexity. Ensure that your systems can detect nested query patterns and either reject or rate-limit them. Again, WAFs can be useful in filtering out such complex, resource-intensive requests.", 12 | "Verbose Errors": "Errors that provide detailed diagnostic information, often useful for debugging but can reveal system internals, configurations, or other sensitive data when displayed to end users. Attackers can exploit such verbose messages to gain insights into potential vulnerabilities within the system.", 13 | "VE Solution": "Use generic error messages in your production environments. Detailed errors can be logged internally for debugging purposes but should never be displayed to end-users. This prevents attackers from gaining insights from the error outputs.", 14 | "Circular DoS Queries": "Attackers send specially crafted SQL queries that create infinite loops or recursive operations in the database. Such queries can consume extensive server resources, leading to denial of service as the database gets overwhelmed and can't handle other requests.", 15 | "CDQ Solution": "Set reasonable timeouts for database queries. Implement monitoring tools that can detect unusual spikes in resource consumption or endless loop patterns, alerting administrators to potential attacks.", 16 | "INJ-URI": "https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#injection-prevention", 17 | "VE-URI": "https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#dont-return-excessive-errors", 18 | "BATCH-URI": "https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#mitigating-batching-attacks", 19 | "DoS-URI": "https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#dos-prevention" 20 | } -------------------------------------------------------------------------------- /server/pentesting/batching.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module batching.ts 5 | * @author MADR Productions - AY 6 | * @date 9-25-23 7 | * @description middleware for server.use('/injection') to generate and send queries to test for SQL injection and evaluate response 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import { Request, Response } from 'express'; 13 | import { 14 | PentestType, 15 | GraphQLType, 16 | QueryResult, 17 | } from '../functionsAndInputs/types.ts'; 18 | import { 19 | generateBatchQuery, 20 | generateQueryNested, 21 | } from '../functionsAndInputs/generateHelper.ts'; 22 | import { 23 | createQueryResult, 24 | createErrorResult, 25 | } from '../functionsAndInputs/query.ts'; 26 | import { batchTitles } from '../functionsAndInputs/titles.ts'; 27 | import { batchingErrorKeywords } from '../functionsAndInputs/inputsAndKeywords.ts'; 28 | import { info } from '../info.ts'; 29 | 30 | 31 | export const batching: PentestType = { 32 | generateQueries: async (req: Request, res: Response) => { 33 | console.log('Generating Batching Queries...'); 34 | const schemaTypes: GraphQLType[] = res.locals.schema.data.__schema.types; 35 | 36 | /* 37 | Generates a nested GraphQL query string for selecting subfields 38 | of a given GraphQL type up to a specified maximum depth. 39 | */ 40 | 41 | const arrOfQueries: (string | string[])[] = []; 42 | const arrOfNested: string[] = []; 43 | 44 | for (const typeName of ['queryType', 'mutationType']) { 45 | const name: string | null = 46 | res.locals.schema.data.__schema[typeName]?.name; 47 | if (!name) continue; 48 | 49 | const types: GraphQLType | undefined = schemaTypes.find( 50 | (type) => type.name === name, 51 | ); 52 | if (!types?.fields) continue; 53 | 54 | for (const field of types.fields) { 55 | const singularQuery: string = generateBatchQuery( 56 | field, 57 | typeName, 58 | schemaTypes, 59 | ); 60 | const identicalQuery: string[] = new Array(10).fill(singularQuery); 61 | arrOfQueries.push(identicalQuery); 62 | const nestedQuery = generateQueryNested(field, typeName, schemaTypes); 63 | arrOfNested.push(nestedQuery); 64 | } 65 | } 66 | arrOfQueries.push(arrOfNested); 67 | res.locals.batchingQueries = arrOfQueries; 68 | console.log('Generated Batching Queries...'); 69 | return; 70 | }, 71 | attack: async (req: Request, res: Response): Promise => { 72 | console.log('Sending Batching Queries...'); 73 | 74 | const results: QueryResult[] = []; 75 | const API: string = req.body.API; 76 | let ID: number = 1; 77 | 78 | const sendReqAndEvaluate = async (query: string): Promise => { 79 | const queryResult = createQueryResult('BATCH', query, ID); 80 | const errorResult = createErrorResult('BATCH', query, ID); 81 | ID++; 82 | 83 | try { 84 | const sendTime = Date.now(); 85 | 86 | const data = await fetch(API, { 87 | method: 'POST', 88 | headers: { 89 | 'Content-Type': 'application/graphql', 90 | }, 91 | body: query, 92 | }).catch((err) => console.log(err)); 93 | 94 | if (!data) { 95 | return errorResult; 96 | } 97 | 98 | const response = await data.json(); 99 | const timeTaken = Date.now() - sendTime; 100 | queryResult.testDuration = `${timeTaken} ms`; 101 | queryResult.details.response = response; 102 | queryResult.details.link = info['BATCH-URI']; 103 | 104 | if (query[0] === query[1]) { 105 | queryResult.title = batchTitles.identical; 106 | queryResult.details.description = 107 | info['Multiple Identical Batching Attack']; 108 | queryResult.details.solution = info['MIBA Solution']; 109 | } else { 110 | queryResult.title = batchTitles.exhaustive; 111 | queryResult.details.description = 112 | info['Resource Exhaustive and Nested Batching Attack']; 113 | queryResult.details.solution = info['REANBA Solution']; 114 | } 115 | 116 | if (response.data) { 117 | queryResult.status = 'Fail'; 118 | queryResult.details.error = 119 | 'Batching is Enabled, Rate Limiting Not Found'; 120 | } 121 | if (response.errors) { 122 | if ( 123 | response.errors.some((error: { message: string }) => 124 | batchingErrorKeywords.some((keyword) => 125 | error.message.toLowerCase().includes(keyword), 126 | ), 127 | ) 128 | ) { 129 | queryResult.status = 'Fail'; 130 | queryResult.details.error = 131 | 'Potential Exposure of Sensitive Information Through Error Message'; 132 | } 133 | } 134 | return queryResult; 135 | } catch (err) { 136 | console.log(err); 137 | return errorResult; 138 | } 139 | }; 140 | const arrofQueries: string[] = res.locals.batchingQueries; 141 | 142 | for (const query of arrofQueries) { 143 | const result = await sendReqAndEvaluate(query); 144 | results.push(result); 145 | } 146 | return results; 147 | }, 148 | }; 149 | -------------------------------------------------------------------------------- /server/functionsAndInputs/generateHelper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLType, 3 | GraphQLField, 4 | GraphQLArgs, 5 | GraphQLTypeReference, 6 | QueryResult, 7 | } from './types'; 8 | 9 | //Recursive function to get base type of any field 10 | export const getBaseType = (type: GraphQLTypeReference): string => { 11 | let curr = type; 12 | while (curr.ofType) { 13 | curr = curr.ofType; 14 | } 15 | return curr.name || ''; 16 | }; 17 | //Subfield generator for non-nested queries given the type 18 | export const getSubFields = ( 19 | type: GraphQLType | GraphQLTypeReference, 20 | schemaTypes: GraphQLType[], 21 | depth: number = 0, 22 | maxDepth: number = 1, 23 | ): string => { 24 | if (!type.fields || depth > maxDepth) return ''; 25 | return `{ ${type.fields 26 | .filter((field) => { 27 | const baseTypeName = getBaseType(field.type); 28 | const baseType = schemaTypes.find((t) => t.name === baseTypeName); 29 | return !baseType?.fields; 30 | }) 31 | .map((field) => field.name) 32 | .join(' ')} }`; 33 | }; 34 | 35 | //function to generate SQL Queries 36 | export const generateSQLQuery = ( 37 | field: GraphQLField, 38 | input: string, 39 | QueryType: string, 40 | schemaTypes: GraphQLType[], 41 | ) => { 42 | const queryName = QueryType === 'queryType' ? 'query' : 'mutation'; 43 | const args = 44 | field.args 45 | ?.filter( 46 | (arg) => arg.type?.kind === 'SCALAR' && arg.type?.name === 'String', 47 | ) 48 | .map((arg) => `${arg.name}: "${input}"`) 49 | .join(', ') || ''; 50 | 51 | const baseTypeName = field.type ? getBaseType(field.type) : ''; 52 | const baseType = schemaTypes.find((type) => type.name === baseTypeName); 53 | const subFields = baseType ? getSubFields(baseType, schemaTypes) : ''; 54 | 55 | return `${queryName} { ${field.name}(${args}) ${subFields} }`; 56 | }; 57 | 58 | //Function to generate queries given field with no type arguments 59 | export const generateBatchQuery = ( 60 | field: GraphQLField, 61 | QueryType: string, 62 | schemaTypes: GraphQLType[], 63 | ) => { 64 | const queryName = QueryType === 'queryType' ? 'query' : 'mutation'; 65 | 66 | const baseTypeName = field.type ? getBaseType(field.type) : ''; 67 | const baseType = schemaTypes.find((type) => type.name === baseTypeName); 68 | const subFields = baseType ? getSubFields(baseType, schemaTypes) : ''; 69 | 70 | return `${queryName} { ${field.name} ${subFields} }`; 71 | }; 72 | 73 | //function to get nested sub-fields given type and depth-- can be adjusted depending on levels of nesting wanted 74 | export const getSubFieldsNested = ( 75 | type: GraphQLType | GraphQLTypeReference, 76 | schemaTypes: GraphQLType[], 77 | depth: number = 0, 78 | maxDepth: number = 1, 79 | ): string => { 80 | // If the type has no fields or reached the maximum depth, return an empty string 81 | if (!type.fields || depth > maxDepth) return ''; 82 | 83 | // Filter and concat valid subfields for the current type 84 | const validSubFields = type.fields 85 | .map((field) => { 86 | const baseTypeName = field.type ? getBaseType(field.type) : ''; 87 | const baseType = schemaTypes.find((t) => t.name === baseTypeName); 88 | 89 | // If reached max depth, return the field name if it has no subfields, 90 | // otherwise, return an empty string 91 | if (depth === maxDepth) { 92 | return !baseType?.fields ? field.name : ''; 93 | } 94 | 95 | // Recursively get subfields for the baseType and concat them 96 | const subFields = baseType 97 | ? getSubFieldsNested(baseType, schemaTypes, depth + 1, maxDepth) 98 | : ''; 99 | 100 | return subFields ? `${field.name} ${subFields}` : ''; 101 | }) 102 | .filter(Boolean) 103 | .join(' '); 104 | 105 | // If there are no valid subfields, return an empty string 106 | if (!validSubFields) return ''; 107 | 108 | // Return the selected subfields within curly braces as a GraphQL query string 109 | return `{ ${validSubFields} }`; 110 | }; 111 | 112 | //function to generate nested queries without any arguments 113 | export const generateQueryNested = ( 114 | field: GraphQLField, 115 | QueryType: string, 116 | schemaTypes: GraphQLType[], 117 | ) => { 118 | const queryName = QueryType === 'queryType' ? 'query' : 'mutation'; 119 | 120 | const baseTypeName = field.type ? getBaseType(field.type) : ''; 121 | const baseType = schemaTypes.find((type) => type.name === baseTypeName); 122 | const subFields = baseType ? getSubFieldsNested(baseType, schemaTypes) : ''; 123 | 124 | return `${queryName} { ${field.name} ${subFields} }`; 125 | }; 126 | export const getVerboseSubFields = ( 127 | type: GraphQLType | GraphQLTypeReference, 128 | schemaTypes: GraphQLType[], 129 | depth: number = 0, 130 | maxDepth: number = 1, 131 | ): string => { 132 | if (!type.fields || depth > maxDepth) return ''; 133 | return `{ ${type.fields 134 | .filter((field) => { 135 | const baseTypeName = getBaseType(field.type); 136 | const baseType = schemaTypes.find((t) => t.name === baseTypeName); 137 | return !baseType?.fields; 138 | }) 139 | .map((field) => field.name) 140 | .join('E ')} }`; 141 | }; 142 | export const generateVerboseQuery = ( 143 | field: GraphQLField, 144 | QueryType: string, 145 | schemaTypes: GraphQLType[], 146 | ) => { 147 | const queryName = QueryType === 'queryType' ? 'query' : 'mutation'; 148 | const baseTypeName = field.type ? getBaseType(field.type) : ''; 149 | const baseType = schemaTypes.find((type) => type.name === baseTypeName); 150 | const subFields = baseType ? getVerboseSubFields(baseType, schemaTypes) : ''; 151 | 152 | return `${queryName} { ${field.name} ${subFields} }`; 153 | }; 154 | -------------------------------------------------------------------------------- /server/pentesting/verboseError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module verboseError.ts 5 | * @author MADR Productions - RP 6 | * @date 9-28-23 7 | * @description middleware for server.use('/error') to generate and send queries to trigger an error and evaluate response for verbosity (a security vulnerability in GraphQL APIs) 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import { Request, Response, NextFunction } from 'express'; 13 | import { 14 | PentestType, 15 | GraphQLType, 16 | GraphQLField, 17 | GraphQLArgs, 18 | GraphQLTypeReference, 19 | QueryResult, 20 | } from '../functionsAndInputs/types.ts'; 21 | import { 22 | getBaseType, 23 | generateBatchQuery, 24 | getSubFieldsNested, 25 | generateQueryNested, 26 | } from '../functionsAndInputs/generateHelper.ts'; 27 | import { generateVerboseQuery } from '../functionsAndInputs/generateHelper.ts'; 28 | import { 29 | createQueryResult, 30 | createErrorResult, 31 | } from '../functionsAndInputs/query.ts'; 32 | import { info } from '../info.ts'; 33 | import { verboseErrorKeywords } from '../functionsAndInputs/inputsAndKeywords.ts'; 34 | 35 | // move all of the following types and interfaces to a TS file 36 | // also can some of them be removed they seem duplicative 37 | 38 | export const verboseError: PentestType = { 39 | // method for generating queries that trigger an error response by purposefully creating a typo 40 | generateQueries: async (req: Request, res: Response) => { 41 | console.log('Generating Queries for verbose...'); 42 | 43 | // types is an array of objects retrieved from getSchema and includes all of the query, mutation, and subscription types 44 | const schemaTypes: GraphQLType[] = res.locals.schema.data.__schema.types; 45 | 46 | const arrOfQueries: string[] = []; 47 | 48 | // Identifies the naming convention for queryType and mutationType 49 | // For example, DVGA calls them "Query" and "Mutations." 50 | // Then look for the object with the name "Query" and check for fields. 51 | // Fields represent the names of the different Queries. 52 | // For example, the Query fields for DVGA include "pastes", "paste", "users" 53 | // Args are opportunities for client input to filter data or mutate data. 54 | for (const typeName of ['queryType', 'mutationType']) { 55 | const name: string | null = 56 | res.locals.schema.data.__schema[typeName]?.name; 57 | if (!name) continue; 58 | 59 | const types: GraphQLType | undefined = schemaTypes.find( 60 | (type) => type.name === name, 61 | ); 62 | if (!types?.fields) continue; 63 | 64 | for (const field of types.fields) { 65 | const query = generateVerboseQuery(field, typeName, schemaTypes); 66 | arrOfQueries.push(query); 67 | } 68 | } 69 | res.locals.queries = arrOfQueries; 70 | console.log('Generated Verbose Queries...'); 71 | return; 72 | }, 73 | attack: async (req: Request, res: Response, _next: NextFunction) => { 74 | console.log('Sending Verbose Queries...'); 75 | 76 | const result: QueryResult[] = []; 77 | const API: string = req.body.API; 78 | let ID: number = 1; 79 | 80 | // Think about logic of the query result and how we define each variable, should we summarize and use accordion? The send req could be modularized. 81 | const sendReqAndEvaluate = async (query: string) => { 82 | const queryResult = createQueryResult('VE', query, ID); 83 | const errorResult = createErrorResult('VE', query, ID); 84 | ID++; 85 | try { 86 | const sendTime = Date.now(); // checks for the time to send and receive respsonse from GraphQL API 87 | 88 | const data = await fetch(API, { 89 | method: 'POST', 90 | headers: { 91 | 'Content-Type': 'application/graphql', 92 | }, 93 | body: query, 94 | }).catch((err) => console.log(err)); 95 | 96 | if (!data) return errorResult; 97 | 98 | const response = await data.json(); 99 | const timeTaken = Date.now() - sendTime; 100 | queryResult.title = 'Verbose Error'; 101 | queryResult.testDuration = `${timeTaken} ms`; 102 | queryResult.details.response = response; 103 | queryResult.details.description = info['Verbose Errors']; 104 | queryResult.details.solution = info['VE Solution']; 105 | queryResult.details.link = info['VE-URI']; 106 | 107 | // Currently, pass/fail is based on error message length, but we could also look at "Did you mean..." to see if the response includes sugggested fields 108 | if (response.errors) { 109 | if (response.errors[0].message.length > 100) { 110 | queryResult.status = 'Fail'; 111 | queryResult.details.error = 112 | 'Error Message is Highly Detailed and May Potentially Leak Sensitive Information'; 113 | } else if ( 114 | response.errors.some((error: { message: string }) => 115 | verboseErrorKeywords.some((keyword) => 116 | error.message.toLowerCase().includes(keyword), 117 | ), 118 | ) 119 | ) { 120 | queryResult.status = 'Fail'; 121 | queryResult.details.error = 122 | 'Error Message is Highly Detailed and May Potentially Leak Sensitive Information'; 123 | } else queryResult.status = 'Pass'; 124 | } 125 | result.push(queryResult); 126 | } catch (err) { 127 | console.log(err); 128 | return errorResult; 129 | } 130 | }; 131 | const arrofQueries = res.locals.queries; 132 | for (const query of arrofQueries) { 133 | await sendReqAndEvaluate(query); 134 | } 135 | return result; 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /server/pentesting/injection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module injection.ts 5 | * @author MADR Productions - AY 6 | * @date 9-25-23 7 | * @description middleware for server.use('/injection') to generate and send queries to test for SQL injection and evaluate response 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import { Request, Response } from 'express'; 13 | import { 14 | PentestType, 15 | GraphQLType, 16 | QueryResult, 17 | } from '../functionsAndInputs/types.ts'; 18 | import { 19 | getBaseType, 20 | getSubFields, 21 | generateSQLQuery, 22 | } from '../functionsAndInputs/generateHelper.ts'; 23 | import { 24 | SQLInputs, 25 | sqlErrorKeywords, 26 | } from '../functionsAndInputs/inputsAndKeywords.ts'; 27 | import { SQLtitles } from '../functionsAndInputs/titles.ts'; 28 | import { 29 | createQueryResult, 30 | createErrorResult, 31 | } from '../functionsAndInputs/query.ts'; 32 | import { info } from '../info.ts'; 33 | 34 | export const injection: PentestType = { 35 | generateQueries: async (req: Request, res: Response) => { 36 | console.log('Generating SQL Injection Queries...'); 37 | const schemaTypes: GraphQLType[] = res.locals.schema.data.__schema.types; 38 | 39 | const arrOfQueries: string[] = []; 40 | 41 | for (const typeName of ['queryType', 'mutationType']) { 42 | const name: string | null = 43 | res.locals.schema.data.__schema[typeName]?.name; 44 | if (!name) continue; 45 | 46 | const types: GraphQLType | undefined = schemaTypes.find( 47 | (type) => type.name === name, 48 | ); 49 | if (!types?.fields) continue; 50 | 51 | for (const field of types.fields) { 52 | if ( 53 | !field.args || 54 | field.args.some( 55 | (arg) => arg.type?.kind == 'SCALAR' && arg.type?.name === 'String', 56 | ) 57 | ) { 58 | for (const input of SQLInputs) { 59 | const query = generateSQLQuery(field, input, typeName, schemaTypes); 60 | arrOfQueries.push(query); 61 | } 62 | } 63 | } 64 | } 65 | res.locals.SQLQueries = arrOfQueries; 66 | console.log('Generated Injection Queries...'); 67 | return; 68 | }, 69 | attack: async (req: Request, res: Response): Promise => { 70 | console.log('Sending SQL Injections...'); 71 | 72 | const results: QueryResult[] = []; 73 | const API: string = req.body.API; 74 | let ID: number = 1; 75 | 76 | const sendReqAndEvaluate = async (query: string): Promise => { 77 | const queryResult = createQueryResult('INJ', query, ID); 78 | const errorResult = createErrorResult('INJ', query, ID); 79 | ID++; 80 | 81 | try { 82 | const sendTime = Date.now(); 83 | 84 | const data = await fetch(API, { 85 | method: 'POST', 86 | headers: { 87 | 'Content-Type': 'application/graphql', 88 | }, 89 | body: query, 90 | }).catch((err) => console.log(err)); 91 | 92 | if (!data) { 93 | return errorResult; 94 | } 95 | 96 | const response = await data.json(); 97 | const timeTaken = Date.now() - sendTime; 98 | queryResult.testDuration = `${timeTaken} ms`; 99 | queryResult.details.response = response; 100 | queryResult.details.link = info['INJ-URI']; 101 | 102 | if (query.includes('OR 1') || query.includes("OR '1")) { 103 | queryResult.title = SQLtitles.booleanBased; 104 | queryResult.details.description = info['Boolean Based SQL Injection']; 105 | queryResult.details.solution = info['BBSQLI Solution']; 106 | } else if ( 107 | query.toLowerCase().includes('sleep') || 108 | query.toLowerCase().includes('delay') 109 | ) { 110 | queryResult.title = SQLtitles.timeBased; 111 | queryResult.details.description = 112 | info['Time-Based Blind SQL Injection']; 113 | queryResult.details.solution = info['TMSQLI Solution']; 114 | } else if ( 115 | query.includes("'") || 116 | query.includes(';') || 117 | query.includes('--') 118 | ) { 119 | queryResult.title = SQLtitles.errorBased; 120 | queryResult.details.description = info['Error Based SQL Injection']; 121 | queryResult.details.solution = info['EBSQLI Solution']; 122 | } 123 | 124 | if (response.data) { 125 | const keys: string[] = Object.keys(response.data); 126 | for (const key of keys) { 127 | if ( 128 | Array.isArray(response.data[key]) && 129 | response.data[key].length > 1 130 | ) { 131 | queryResult.status = 'Fail'; 132 | queryResult.details.error = 133 | 'Potentially Excessive/Sensitive Information Given'; 134 | } 135 | } 136 | } 137 | 138 | if ( 139 | response.errors && 140 | response.errors.some((error: { message: string }) => 141 | sqlErrorKeywords.some((keyword) => 142 | error.message.toLowerCase().includes(keyword), 143 | ), 144 | ) 145 | ) { 146 | queryResult.status = 'Fail'; 147 | queryResult.details.error = 148 | 'Potential Exposure of Sensitive Information Through Error Message'; 149 | } 150 | if (timeTaken > 5000) { 151 | queryResult.status = 'Fail'; 152 | queryResult.details.error = 153 | 'Server Response Delayed Due to Injection'; 154 | } 155 | return queryResult; 156 | } catch (err) { 157 | console.log(err); 158 | return errorResult; 159 | } 160 | }; 161 | const arrofQueries: string[] = res.locals.SQLQueries; 162 | 163 | for (const query of arrofQueries) { 164 | const result = await sendReqAndEvaluate(query); 165 | results.push(result); 166 | } 167 | return results; 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /src/stylesheets/App.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | // Main Container Styling 4 | 5 | body { 6 | background-color: $background-color; 7 | font-family: 'Roboto', sans-serif; 8 | } 9 | 10 | .img_responsive { 11 | width: 3em; 12 | height: 3em; 13 | } 14 | 15 | .main_header { 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | font-size: 1.3em; 20 | color: $primary-color; 21 | margin-left: 7px; 22 | } 23 | 24 | #QL { 25 | color: $secondary-color; 26 | } 27 | 28 | .dashboard__headers { 29 | color: $primary-color; 30 | margin-left: 10px; 31 | margin-bottom: 12px; 32 | font-size: 30px; 33 | } 34 | 35 | .main__container { 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: flex-start; 39 | padding: 20px; 40 | } 41 | 42 | .input_form { 43 | width: 100%; /* Occupy full width of the container */ 44 | margin-bottom: 20px; /* Add space below the form */ 45 | } 46 | 47 | input { 48 | padding-left: 10px; 49 | } 50 | 51 | .underline { 52 | width: auto; 53 | height: 3px; /* Adjust the height to control the thickness of the line */ 54 | margin: 0 10px 20px; 55 | background-image: linear-gradient( 56 | 90deg, 57 | $primary-color 0%, 58 | $secondary-color 50%, 59 | $tertiary-color 100% 60 | ); 61 | background-clip: content-box; 62 | padding: 1px; /* This will act as the border */ 63 | } 64 | 65 | // Textbox Styling 66 | 67 | #textbox { 68 | height: 35px; 69 | width: 100%; 70 | max-width: 400px; 71 | font-size: 15px; 72 | margin: 0 10px 20px; 73 | border-radius: 5px; 74 | border: 2px solid $border-color; 75 | background-color: $ten-color; 76 | } 77 | #textbox:focus { 78 | outline: none; 79 | border: 2px solid $secondary-color; 80 | } 81 | 82 | // Checkbox Styling 83 | 84 | .switch { 85 | position: relative; 86 | display: flex; 87 | align-items: center; 88 | width: 63px; 89 | height: 29px; 90 | margin: 0 10px; 91 | } 92 | .switch input { 93 | display: none; 94 | } 95 | 96 | .tests { 97 | display: flex; 98 | margin-bottom: 25px; 99 | margin-right: 10px; 100 | align-items: center; 101 | } 102 | 103 | .slider { 104 | position: absolute; 105 | cursor: pointer; 106 | top: 0; 107 | left: 0; 108 | right: 5px; 109 | bottom: 0; 110 | transition: 0.4s; 111 | border-radius: 20px; 112 | background-color: $slider-color; 113 | } 114 | 115 | .slider:before { 116 | position: absolute; 117 | content: ''; 118 | height: 29px; 119 | width: 30px; 120 | left: 0px; 121 | top: 0px; 122 | background-color: $primary-color; 123 | transition: 0.4s; 124 | border-radius: 20px; 125 | } 126 | 127 | input:checked + .slider { 128 | background-color: $slider-color-selected; 129 | } 130 | 131 | input:checked + .slider:before { 132 | transform: translateX(28px); 133 | } 134 | 135 | .switch input:checked + .slider { 136 | background-color: $slider-color-selected; 137 | } 138 | 139 | .switch input:checked + .slider:before { 140 | transform: translateX(28px); 141 | } 142 | 143 | .checkbox { 144 | height: auto; 145 | } 146 | 147 | .text { 148 | display: flex; 149 | flex: 1; 150 | flex-direction: column; 151 | font-size: 15px; 152 | } 153 | 154 | // Button Styling 155 | 156 | #submit_button { 157 | border-radius: 6px; 158 | height: 44px; 159 | margin-left: 10px; 160 | margin-top: 10px; 161 | padding: 0 25px; 162 | width: 100px; 163 | } 164 | 165 | #select_all_button { 166 | border-radius: 6px; 167 | height: 44px; 168 | margin-left: 10px; 169 | margin-top: 10px; 170 | padding: 0 25px; 171 | width: 100px; 172 | float: left; 173 | margin: 30px; 174 | margin-left: 10px; 175 | margin-top: 10px; 176 | } 177 | 178 | .buttons { 179 | appearance: button; 180 | backface-visibility: hidden; 181 | border-width: 0; 182 | box-shadow: 183 | $ten-color 0 0 0 1px inset, 184 | $ten-color 0 2px 5px 0, 185 | rgba($black, 0.07) 0 1px 1px 0; 186 | box-sizing: border-box; 187 | color: $white; 188 | cursor: pointer; 189 | font-family: -apple-system, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', 190 | Ubuntu, sans-serif; 191 | font-size: 100%; 192 | line-height: 1.15; 193 | outline: none; 194 | overflow: hidden; 195 | position: relative; 196 | text-align: center; 197 | text-transform: none; 198 | transform: translateZ(0); 199 | transition: 200 | all 0.2s, 201 | box-shadow 0.08s ease-in, 202 | background-color 0.2s; 203 | user-select: none; 204 | -webkit-user-select: none; 205 | touch-action: manipulation; 206 | border-radius: 3px; 207 | padding: 5px 5px; 208 | width: auto; 209 | background-color: $primary-color; 210 | &:hover { 211 | background-color: $secondary-color; 212 | } 213 | } 214 | 215 | // Scan Results Styling 216 | 217 | .results-table-export__container { 218 | display: flex; 219 | flex-direction: row; 220 | justify-content: space-between; 221 | margin-bottom: 5px; 222 | margin-left: 10px; 223 | margin-right: 10px; 224 | padding: 0; 225 | } 226 | 227 | .dashboard__container { 228 | background-color: $white; 229 | margin: 20px; 230 | padding: 0 20px 20px 20px; 231 | border: 1px solid $border-color; 232 | border-radius: 10px; 233 | box-shadow: 234 | $seventy-color 3px 3px 2px, 235 | $fifty-color 6px 6px 3px, 236 | $thirty-color 9px 9px 3px, 237 | $ten-color 12px 12px 4px, 238 | $five-color 15px 15px 4px; 239 | } 240 | 241 | .loader__container { 242 | display: flex; 243 | flex-direction: column; 244 | justify-content: center; 245 | align-items: center; 246 | color: $primary-color; 247 | } 248 | 249 | // Modal Styling 250 | 251 | .ReactCollapse--collapse { 252 | transition: height 500ms; 253 | } 254 | 255 | .ReactModal__Body--open, 256 | .ReactModal__Html--open { 257 | overflow: hidden; 258 | } 259 | 260 | #modal-close__button { 261 | display: flex; 262 | flex-direction: row-reverse; 263 | justify-content: space-between; 264 | margin-top: 10px; 265 | margin-right: 10px; 266 | } 267 | 268 | .modal-details__container { 269 | margin: 0 10px; 270 | padding-right: 20px; 271 | max-height: 60vh; 272 | overflow-y: scroll; 273 | min-width: 500px; 274 | } 275 | 276 | #modal-underline { 277 | margin-bottom: 0; 278 | } 279 | 280 | #modal__header { 281 | margin-top: 0; 282 | } 283 | -------------------------------------------------------------------------------- /server/pentesting/circularQuery.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-loop-func */ 2 | /** 3 | * ************************************ 4 | * 5 | * @module circularQuery.ts 6 | * @author MADR Productions - MK & RP 7 | * @date 10-05-23 8 | * @description searches for circular references within the object types in the schema, generates and executes circular queries when found, and returns a pass or fail depending on if the number of nested relationships exceeds the maximum allowable depth 9 | * 10 | * ************************************ 11 | */ 12 | import { Request, Response, NextFunction } from 'express'; 13 | import { 14 | PentestType, 15 | GraphQLType, 16 | GraphQLField, 17 | QueryResult, 18 | } from '../functionsAndInputs/types.ts'; 19 | import { 20 | createQueryResult, 21 | createErrorResult, 22 | } from '../functionsAndInputs/query.ts'; 23 | import { info } from '../info.ts'; 24 | 25 | 26 | // Helper function that searches for circular references within the schema 27 | export const circularQuery: PentestType = { 28 | generateQueries: async (req: Request, res: Response) => { 29 | console.log('Generating circular queries...'); 30 | // Retrieves all the operation types "Query, Mutation, Subscription" in the schema 31 | const schemaTypes: GraphQLType[] = res.locals.schema.data.__schema.types; 32 | const field_1_name: string = ''; 33 | const field_2_name: string = ''; 34 | const scalar_name: string = ''; 35 | const firstObjName: string = ''; 36 | const objTypeName: string = ''; 37 | const queries: unknown[] = []; 38 | const allRelationships: unknown[] = []; // contains all circularly referenced field names along with one scalar field to end the loop 39 | 40 | // Check if mutationType and subscriptionType exist in the schema 41 | const queryTypeName = res.locals.schema.data.__schema.queryType?.name; 42 | const mutationTypeName = res.locals.schema.data.__schema.mutationType?.name; 43 | const subscriptionTypeName = 44 | res.locals.schema.data.__schema.subscriptionType?.name; 45 | 46 | const queryTypeObject = schemaTypes.filter( 47 | (object) => object.name === queryTypeName, 48 | ); 49 | const queryTypeFields = queryTypeObject[0].fields; 50 | 51 | const interfaceObject = queryTypeFields?.filter( 52 | (object) => object.type.kind === 'INTERFACE', 53 | ); 54 | 55 | let interfaceTypeName: string = ''; 56 | 57 | if (interfaceObject?.length !== 0) { 58 | interfaceTypeName = interfaceObject![0].name; 59 | } 60 | 61 | // Create an array of type names to exclude 62 | const excludedTypeNames = [ 63 | queryTypeName, 64 | mutationTypeName, 65 | subscriptionTypeName, 66 | ].filter((name) => name); // Filter out null values 67 | 68 | // Filter out objects that are query, mutation, and subscription types 69 | const customNameTypes = schemaTypes.filter( 70 | (object) => 71 | !excludedTypeNames.includes(object.name) && 72 | object.name[0] !== '_' && 73 | object.kind === 'OBJECT', 74 | ); 75 | 76 | const circularRefs: Set = new Set(); 77 | 78 | function addUniqueTuple(tuple: [string, string, string]): void { 79 | const tupleString = JSON.stringify(tuple); 80 | circularRefs.add(tupleString); 81 | } 82 | const findCircularRelationships = ( 83 | nameType: any, 84 | ): [string, string, string][] => { 85 | let primFieldName: string = ''; 86 | let secFieldName: string = ''; 87 | let scalarName: string = ''; 88 | 89 | const traverseFields = ( 90 | typeName: string, 91 | fields: GraphQLField[] | undefined, 92 | visitedFields: Set, 93 | ) => { 94 | if ( 95 | visitedFields.has(typeName!) && 96 | primFieldName !== '' && 97 | secFieldName !== '' && 98 | scalarName !== '' 99 | ) { 100 | addUniqueTuple([primFieldName, secFieldName, scalarName]); 101 | return circularRefs; 102 | } 103 | visitedFields.add(typeName); 104 | fields?.forEach((field) => { 105 | const fieldType = field.type.ofType || field.type; 106 | if (field.name === interfaceTypeName) return; 107 | 108 | if (fieldType?.kind === 'OBJECT') { 109 | const foundQueryType = queryTypeFields?.find( 110 | (obj) => obj.name === field.name, 111 | ); 112 | if (foundQueryType !== undefined) { 113 | primFieldName = field.name; 114 | } else { 115 | secFieldName = field.name; 116 | // Find a scalar field on the second object 117 | const circularType = nameType.find( 118 | (t: any) => t.name === fieldType.name, 119 | ); 120 | const scalarField = circularType?.fields.find( 121 | (f: GraphQLField) => f.type.kind === 'SCALAR', 122 | ); 123 | if (scalarField) { 124 | scalarName = scalarField.name; 125 | } 126 | } 127 | traverseFields( 128 | fieldType.name!, 129 | nameType.find((t: any) => t.name === fieldType.name)?.fields, 130 | new Set(visitedFields), 131 | ); 132 | } 133 | }); 134 | }; 135 | 136 | customNameTypes.forEach((customType) => { 137 | traverseFields(customType.name, customType.fields, new Set()); 138 | }); 139 | 140 | const uniqueTuples: [string, string, string][] = Array.from( 141 | circularRefs, 142 | (tupleString) => JSON.parse(tupleString), 143 | ); 144 | return uniqueTuples; 145 | }; 146 | 147 | const circularReferences = findCircularRelationships(customNameTypes); 148 | 149 | const buildQuery = ( 150 | field_1: string, 151 | field_2: string, 152 | scalar: string, 153 | ): void => { 154 | const FIELD_REPEAT = 10; 155 | let query = 'query {'; 156 | let count = 0; 157 | 158 | for (let i = 0; i < FIELD_REPEAT; i++) { 159 | count++; 160 | const closingBraces = '}'.repeat(FIELD_REPEAT * 2) + '}'; 161 | const payload = `${field_1} { ${field_2} { `; 162 | query += payload; 163 | 164 | if (count === FIELD_REPEAT) { 165 | query += scalar + closingBraces; 166 | } 167 | } 168 | queries.push(query); 169 | }; 170 | 171 | for (const array of circularReferences) { 172 | buildQuery(array[0], array[1], array[2]); 173 | } 174 | 175 | res.locals.queries = queries; 176 | return; 177 | }, 178 | attack: async (req: Request, res: Response, _next: NextFunction) => { 179 | console.log('Sending Circular Queries...'); 180 | 181 | const result: QueryResult[] = []; 182 | const API: string = req.body.API; 183 | let ID: number = 1; 184 | 185 | const sendReqAndEvaluate = async (query: string) => { 186 | const queryResult = createQueryResult('DoS', query, ID); 187 | const errorResult = createErrorResult('DoS', query, ID); 188 | ID++; 189 | try { 190 | const sendTime = Date.now(); // checks for the time to send and receive respsonse from GraphQL API 191 | 192 | const data = await fetch(API, { 193 | method: 'POST', 194 | headers: { 195 | 'Content-Type': 'application/graphql', 196 | }, 197 | body: query, 198 | }).catch((err) => console.log(err)); 199 | 200 | if (!data) return errorResult; 201 | 202 | const response = await data.json(); 203 | const timeTaken = Date.now() - sendTime; 204 | queryResult.title = 'Denial of Service: Circular'; 205 | queryResult.testDuration = `${timeTaken} ms`; 206 | queryResult.details.response = response; 207 | queryResult.details.description = info['Circular DoS Queries']; 208 | queryResult.details.solution = info['CDQ Solution']; 209 | queryResult.details.link = info['DoS-URI']; 210 | 211 | if (response.errors) { 212 | queryResult.status = 'Pass'; 213 | } else if (response.data) { 214 | queryResult.status = 'Fail'; 215 | queryResult.details.error = 216 | 'Response Data Returned, Depth Limiting not Found'; 217 | } 218 | result.push(queryResult); 219 | console.log(response); 220 | } catch (err) { 221 | console.log(err); 222 | } 223 | }; 224 | const arrofQueries = res.locals.queries; 225 | 226 | // Check to see if there are any circular references 227 | if (arrofQueries.length === 0) { 228 | const defaultQuery = createQueryResult('DoS', 'No Circular Queries', 1); 229 | result.push({ 230 | ...defaultQuery, 231 | status: 'Pass', 232 | title: 'No Circular References Found', 233 | }); 234 | } else { 235 | for (const query of arrofQueries) { 236 | await sendReqAndEvaluate(query); 237 | } 238 | } 239 | return result; 240 | }, 241 | }; 242 | --------------------------------------------------------------------------------