Writing Custom Security Tests

This article was written by the guest expert, Aleksandr Krasnov. Aleksandr is the DevSecOps expert, principal security engineer, and an advisor. He has worked in companies like Meta, Dropbox, and Palo Alto Networks.

Writing Custom Security Tests

Have you ever considered the potential gaps that may arise when relying solely on security tests integrated into your testing tools? If so, you’re in the right place.

In this blog post, we explore the paramount importance of custom security tests. Tailored to your application’s unique features and risk scenarios, these tests provide a strategic advantage in identifying vulnerabilities. They not only reduce organizational risk but also minimize personal risk for security engineers.

Let’s begin by understanding why you need to write custom security test, specifically in the context of implementing it within the DAST (Dynamic Application Security Testing) tools.

Why do you need to write custom security tests for DAST?

DAST is the type of test you want to run against your fully built and running application. It’s often referred to as black-box testing since it’s not looking into the source code of the application but a fully deployed application. This is often the same way that external attackers will see your application.

Therefore, it’s extremely crucial to understand how to write security tests. By writing those, we want to create a simulated set of actions that attackers are most likely to take.

Think of it this way: if an attacker is going to approach your web application by first scanning all the open ports on that given URL and/or enumerate all the subdomains, then perhaps that’s one of the things you should account for in your security tests. This will help you to detect if any internal subdomains and/or ports accidentally got opened to the public.

Examples of custom security tests for DAST with Nuclei

Scenario #1: Web application is scaling up

Let’s look at the following scenario. We have a web application that we are scaling up. The expected number of users is growing each day, and so are our development processes. We want to ensure that we don’t expose SSH port to the public. We can leverage a tool like Nuclei to write a template that will check if port 22 is opened, and alert us on Slack if there is a find:

id: ssh-port-alert
info:
  name: SSH Port Alert
  author: Your Name
  severity: high
  description: |
    This template is designed to detect the presence of an open SSH port (22) and send an alert to Slack.

requests:
  - method: GET
    path:
      - /
    matchers:
      - type: port
        ports:
          - 22
    matchers-condition: or
    matchers:
      - type: status
        status:
          - 200

    notify:
      - slack:
          url: "https://hooks.slack.com/services/your/slack/webhook/url"
          channel: "#your-channel"
          username: "NucleiBot"
          icon_url: "https://your-icon-url.com/icon.png"
          message: "Alert: Open SSH Port (22) detected on {{Hostname}}"

Scenario #2: Subdomain is being worked on internally

We have a team of engineers that is working on the subdomain developers.example.com . We do not want it to be publicly accessible as we haven’t launched the feature just yet. We can create a similar template that checks for a new subdomain and emails us if it’s exposed:

id: public-domain-alert
info:
  name: Public Domain Alert
  author: Your Name
  severity: high
  description: |
    This template is designed to check if developers.example.com is publicly accessible and send an email notification if detected.

requests:
  - method: GET
    path:
      - /
    matchers:
      - type: status
        status:
          - 200

    notify:
      - email:
          to: "your.email@example.com"
          subject: "Public Domain Alert - developers.example.com"
          body: |
            Hello,

            This is an alert to inform you that developers.example.com is publicly accessible.

            Best regards,
            Your Name

Scenario #3: GraphQL development

We are moving from REST API to GraphQL development. We have cases when we have to use introspection for development. However, we want to ensure that introspection is disabled for a graph in production. 

We can write a simple Nuclei template that sends a GraphQL introspection query to the target and looks for specific keywords ("Query," "Mutation," "Subscription") in the response. If any of these keywords are found, it indicates that introspection is enabled :

id: graphql-introspection-check
info:
  name: GraphQL Introspection Check
  author: Your Name
  severity: medium
  description: |
    This template is designed to check if GraphQL introspection is enabled.

requests:
  - method: POST
    path:
      - /
    headers:
      Content-Type: "application/json"
    body: |
      {
        "query": "{__schema{types{name}}}"
      }

    matchers:
      - type: word
        words:
          - "Query"
          - "Mutation"
          - "Subscription"

    notify:
      - webhook:
          url: "https://your-webhook-url.com"
          method: "POST"
          headers:
            Content-Type: "application/json"
          body: '{"message": "GraphQL Introspection Enabled on {{Hostname}}"}'

Implementing Custom Security Tests with Escape

One of the core pain points in custom test implementation with Nuclei is the maintenance of each test, to follow along the attack surface changes, whether they are pure API specification changes, or new APIs that extend your organization’s attack surface.

Thankfully, keeping track of everything exposed is one of the core features of Escape’s API inventory, so you can be notified of any newly exposed APIs and quickly setup a scan (with even more powerful authentication recently released)

Escape’s custom test framework can help you harness the feedback-driven crawling & testing of any API specification.

In this section, we’ll cover the various blocks that make up a custom test in Escape:

For example, you can seed your test with pre-made requests, like Nuclei.

seed:
  - protocol: http
    raw: |
      @Host: https://example.com
      GET /debug HTTP/1.1
      Host: example.com
      Content-Type: application/json

You can simply define what the alert would look like in your Escape dashboard thanks to an Alerting block:

alert:
  name: Deletion successful forced
  context: >
    For compliance reasons, the non admin user must not be able to delete some
    data via the API.
  severity: HIGH

Of course, you can specify detection criterias to trigger the alert with an AND logical operator, and use advanced selectors such as:

  • Response status detector
  • Response duration detector
  • Response success detector
  • Request headers detector
  • Response headers detector
  • Request/Response body JSON detector
  • Request/Response body text detector
  • Request/Response object detector

And many others.

Simple examples might just inspect the response, or more advanced use-cases may combine detections on both the request and the response.

As it was mentioned previously, the language’s abilities are not limited, you can dive into deeper characteristics such as the type of objects contained in each (aka scalars), this is thanks to the engine’s inference system.

You can define your own scalars to detect internal tokens, and then write a detection rule to detect leaks at an organization-scale, it is that simple.

detect:
  - if: response.object
    type:
      in:
        - internal_api_token

But what would be the fun in a scanner if we can modify the expected structure of requests ? This is where transformations come into play.

The transform block is compose of “trigger” & “mutate” actions, here’s a simple example:

transform:
  trigger:
    - if: schema.url
      is: "/api/v1/tested/route"
  mutate:
    - key: request.headers
      name: X-API-version
      value: "APIV2"

Want to validate that all API routes versioned to V1 are not open to V2 clients ?

transform:
  trigger:
    - if: schema.url
      contains: "v1"
  mutate:
    - key: request.headers
      name: X-API-version
      value: "APIV2"

Or maybe you’d like to validate that any email object is only accepting a specific domain, checking all your domain validation logic across the organization’s APIs ?

This transformation will trigger only on specific routes, and will look for email scalars in requests and replace their domain.

transform:
  trigger:
    - if: schema.url
      is: "/api/v1/tested/route"
  mutate:
    - key: request.object
      select:
        type:
          is: email
        name:
          is: "admin_email"
        value:
          regex: .*@escape.tech
      mutate:
        regex_replace:
          pattern: (.*)@escape.tech
          replacement: \1@attacker.com

Support for JQ-style notation is also available to let you manipulate objects in complex ways.

For example, here we could test for Mass-Assignment styles of vulnerabilities by adding new properties to a request object.

transform:
  trigger:
    - if: request.body.json
      is: { "user": "admin" }
  mutate:
    - key: request.body.json
      jq: '. | {"user": .user + " {{modify_value}}"}'

You can read more here about Escape's Custom Test language reference.

Conclusion

As we've showed you with custom security tests, it’s clear that the future of application security is not just in the tools we use, but in how we tailor them to our unique needs.

What does this all mean for you? Well, by creating tests that are tailor-made for your application, you're not just fixing problems as they pop up – you're working to stop them proactively. This way, you're not just reacting to threats, you're outsmarting them.


💡 Want to learn more about testing? Check out the following articles: