Since June 2020, Acunetix supports the increasingly popular API query language – GraphQL. In this article, we want to show you step-by-step how to scan an API defined using GraphQL. To do this, you will first create an intentionally vulnerable API and its GraphQL definition, then scan it using Acunetix, eliminate critical vulnerabilities that you found using Acunetix, and verify that they have been eliminated.

Stage 1: Set Up Your Test Environment

To be able to follow this exercise, you must prepare a test environment. For this exercise, we used the Windows operating system with open-source software.

  1. Install Wamp64 as your local web server with PHP and MySQL
  2. Set the Windows path environment variable to point to your PHP and MySQL executables:
    • Run the Windows executable systempropertiesadvanced.exe
    • Go to the Advanced tab
    • Click on Environment Variables
    • Add your php.exe folder and your mysql.exe folder to the list of paths; typical values to add would be:
      • c:\wamp64\bin\php\php7.3.21
      • c:\wamp64\bin\mysql\mysql5.7.31\bin
  3. Install the Composer dependency manager for PHP

Stage 2: Build a Simple GraphQL API

To scan a GraphQL API with Acunetix, you will build a simple, intentionally vulnerable API. To build the API, you need to perform the following steps:

  1. Create a database on your web server to store your data
  2. Create a web service root folder and install some dependency libraries
  3. Create a GraphQL schema file
  4. Create a GraphQL schema loader file
  5. Create an index file to handle GraphQL requests
  6. Create a resolvers file

Step 1: Create a Database on Your Web Server

To set up a database on your Wamp64 web server, perform the following actions

  1. Open the command prompt
  2. Run mysql -u root
  3. Run the following commands from the MySQL root prompt:
    mysql> create user 'graphuser'@'localhost' identified by 'graphuserpass';
    mysql> create database graphusersdb;
    mysql> GRANT ALL PRIVILEGES ON graphusersdb.* TO 'graphuser'@'localhost';
    mysql> use graphusersdb;
    mysql> CREATE TABLE `users` (`id` int(11) NOT NULL AUTO_INCREMENT,`fname` varchar(255) NOT NULL, `lname` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `notes` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`));
    mysql> INSERT INTO users (fname, lname, email) VALUES ('John', 'Smith', 'john@example.com');
    mysql> INSERT INTO users (fname, lname, email) VALUES ('Jane', 'Doe', 'jane@example.com');
    

Step 2: Create the Web Service Root Folder

To create the web service root folder, perform the following actions:

  1. Open a command prompt
  2. Run the following commands to create the folders for your project, including PHP library dependencies:
    C:\>mkdir C:\wamp64\www\graphusers
    C:\>cd C:\wamp64\www\graphusers
    C:\wamp64\www\graphusers>composer require leocavalcante/siler
    C:\wamp64\www\graphusers>composer require overblog/dataloader-php
    C:\wamp64\www\graphusers>composer require webonyx/graphql-php
    

Step 3: Create a GraphQL Schema File

Create a C:\wamp64\www\graphusers\schema.graphql file with the following content:

type Query {
  getUser(email: String): [User]
}

type Mutation {
  addUser(fname: String, lname: String, email: String, notes: String): [User]
  deluser(email: String): [User]
}

type User {
  id: Int
  fname: String
  lname: String
  email: String
  notes: String
}

Step 4: Create a GraphQL Schema Loader File

Create a C:\wamp64\www\graphusers\schema.php file with the following content:

<?php

use Siler\Graphql;

$typeDefs = file_get_contents(__DIR__.'/schema.graphql');
$resolvers = include __DIR__.'/resolvers.php';

return Graphql\schema($typeDefs, $resolvers);

Step 5: Create an Index File to Handle GraphQL Requests

Create a C:\wamp64\www\graphusers\index.php file with the following content:

<?php

require_once 'vendor/autoload.php';

use Siler\Graphql;
use GraphQL\GraphQL as WGraphql;
use Siler\Http\Request;
use Siler\Http\Response;
use Overblog\DataLoader\DataLoader;
use Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL\SyncPromiseAdapter;
use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter;

try {
     $MyDB = new PDO("mysql:host=localhost;dbname=graphusersdb;charset=utf8mb4", "graphuser", "graphuserpass");
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}

function select_sql($query, $sql_args) {
    global $MyDB;
    $stmt = $MyDB->prepare($query);
    $stmt->execute($sql_args);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    return $rows;
}

function insert_sql($query, $sql_args) {
    global $MyDB;
    $stmt = $MyDB->prepare($query);
    return $stmt->execute($sql_args); // true if successful; false if not successful 
}

function delete_sql($query, $sql_args) {
    global $MyDB;
    $stmt = $MyDB->prepare($query);
    return $stmt->execute($sql_args); // true if successful; false if not successful 
}

$graphQLSyncPromiseAdapter = new SyncPromiseAdapter();
$promiseAdapter = new WebonyxGraphQLSyncPromiseAdapter($graphQLSyncPromiseAdapter);

WGraphQL::setPromiseAdapter($graphQLSyncPromiseAdapter);

$context = [
    'select_sql' => function ($query, $sql_args) { return select_sql($query, $sql_args); },
    'insert_sql' => function ($query, $sql_args) { return insert_sql($query, $sql_args); },
    'delete_sql' => function ($query, $sql_args) { return delete_sql($query, $sql_args); },
];

if (Request\method_is('post')) {
    $schema = include __DIR__.'/schema.php';
    Graphql\init($schema, null, $context);
}

Step 6: Create a Resolvers File

Create a C:\wamp64\www\graphusers\resolvers.php file with the following content:

<?php

return [
  'Query' => [
    'getUser' => function($root, $args, $context) {
    return $context['select_sql']("SELECT id, fname, lname, email, notes FROM users WHERE email=:email", ['email'=>$args['email']]);
        }
    ],
  'Mutation' => [
    'addUser' => function($root, $args, $context) {
      $dummy = $context['insert_sql']("INSERT INTO users (fname, lname, email, notes) VALUES ('" . $args['fname'] . "', '" . $args['lname'] . "', '" . $args['email'] . "', '" . $args['notes'] . "')", []);
      if ($dummy) {
        return $context['select_sql']("SELECT id, fname, lname, email, notes FROM users WHERE email=:email", ['email'=>$args['email']]);
      }
      return null;
    },
    'delUser' => function($root, $args, $context) {
      $delrecord = $context['select_sql']("SELECT id, fname, lname, email, notes FROM users WHERE email=:email", ['email'=>$args['email']]);
      $dummy = $context['delete_sql']("DELETE FROM users WHERE email=:email", ['email'=>$args['email']]);
      if ($dummy) {
        return $delrecord;
      }
      return null;
    }
  ]
];

Stage 3: Scan Your GraphQL API

In this example, the web service definition is at the following URL: http://localhost/graphusers/schema.graphql. To scan the web service with Acunetix:

  1. Create a new target with the following URL: http://localhost/graphusers/
  2. Import an amended schema file to your target:
    • Download the schema file from the following URL: http://localhost/graphusers/schema.graphql
    • Insert one line at the top of the file to specify the GraphQL endpoint; the file should look like this:
      graphql_endpoint="/graphusers/";
      type Query {
        getUser(email: String): [User]
      }
      
      type Mutation {
        addUser(fname: String, lname: String, email: String, notes: String): [User]
        deluser(email: String): [User]
      }
      
      type User {
        id: Int
        fname: String
        lname: String
        email: String
        notes: String
      }
      
    • Locate the Import Files section of your target configuration and import the amended schema file
  3. Launch a Full Scan of your web service and wait for it to complete

Stage 4: Identify Vulnerabilities in Your GraphQL API

Examine the list of vulnerabilities for your scan. We shall concentrate on the SQL injection vulnerabilities for this exercise, since they all have the same root cause.


Acunetix shows the Attack Details — the GraphQL API call had variables injected with delay commands and Acunetix was able to confirm that the responses were indeed delayed by the specified number of seconds. This confirms that the API call is vulnerable to SQL injection, allowing a malicious hacker to craft additional requests to possibly retrieve large volumes of data.

Stage 5: Resolve the Vulnerabilities

A quick look at the addUser mutation function inside the resolvers.php class file can reveal the root cause. The query is built using string concatenation:

$dummy = $context['insert_sql']("INSERT INTO users (fname, lname, email, notes) VALUES ('" . $args['fname'] . "', '" . $args['lname'] . "', '" . $args['email'] . "', '" . $args['notes'] . "')", []);
]

The $args[‘fname’] variable and other variables are being simply concatenated to the query string without any validation. We can adjust the code by using parameterized queries. The newly-adjusted line in the resolvers.php file would look like this:

$dummy = $context['insert_sql']("INSERT INTO users (fname, lname, email, notes) VALUES (:fname, :lname, :email, :notes)", ['fname'=>$args['fname'],  'lname'=>$args['lname'], 'email'=>$args['email'], 'notes'=>$args['notes']]);

Stage 6: Rescan to Confirm Resolution

Go to the list of vulnerabilities for the scan and select the SQL injection vulnerability you have just resolved.

Click on the Retest button — this will create a new scan to test the selected vulnerabilities again. The results will show that you have successfully resolved the vulnerabilities.

SHARE THIS POST
THE AUTHOR
Kevin Attard Compagno
Technical Writer
Kevin Attard Compagno is a Technical Writer working for Acunetix. A technical writer, translator, and general IT buff for over 30 years, Kevin used to run Technical Support teams and create training documents and other material for in-house technical staff.