Active scan check worked example

You can extend Burp Scanner by writing your own scan checks. A custom scan check can report new security issues that Burp doesn't detect natively.

In this worked example, we'll use Java to write an active custom scan check. Active checks send additional, modified requests to probe for vulnerabilities. Use these to detect issues that only appear when requests are manipulated, such as:

Related pages

This example sends two follow-up requests with a forged Origin header. It reports an issue if the server reflects the header in Access-Control-Allow-Origin. The issue severity is raised if Access-Control-Allow-Credentials: true is also present, and a note is created if Vary: Origin is missing:

if (!requestResponse.hasResponse()) { return AuditResult.auditResult(); } var evilHttps = "https://" + api().utilities().randomUtils().randomString(6) + "." + api().utilities().randomUtils().randomString(3); var evilHttp = "http://" + api().utilities().randomUtils().randomString(6) + "." + api().utilities().randomUtils().randomString(3); for (var origin : new String[]{evilHttps, evilHttp}) { var rr = http.sendRequest( requestResponse.request() .withRemovedHeader("Origin") .withAddedHeader("Origin", origin) ); if (!rr.hasResponse()) { continue; } var headers = rr.response().headers().toString().toLowerCase(); var creds = headers.contains("access-control-allow-credentials: true"); var reflect = headers.contains("access-control-allow-origin: " + origin.toLowerCase()); var vary = headers.contains("vary: origin"); if (reflect) { var severity = creds ? AuditIssueSeverity.HIGH : AuditIssueSeverity.MEDIUM; var note = vary ? "" : " (missing Vary: Origin)"; return AuditResult.auditResult( AuditIssue.auditIssue( "CORS: arbitrary origin reflection" + note, "Reflected Origin: " + origin + "; credentials=" + creds, "Use strict allowlist; include Vary: Origin.", rr.request().url(), severity, AuditIssueConfidence.FIRM, "", "", severity, rr ) ); } } return AuditResult.auditResult();

Step 1: Make sure there's a response

if (!requestResponse.hasResponse()) { return AuditResult.auditResult(); }

Before doing anything, the check confirms there's a response to work with. It exits cleanly if a response doesn't exist.

Breakdown of the code

Step 2: Create random origins

var evilHttps = "https://" + api().utilities().randomUtils().randomString(6) + "." + api().utilities().randomUtils().randomString(3); var evilHttp = "http://" + api().utilities().randomUtils().randomString(6) + "." + api().utilities().randomUtils().randomString(3);

This creates two random domains to avoid caching artefacts and ensure the application can't allowlist them.

Breakdown of the code

Step 3: Loop through each origin

for (var origin : new String[]{evilHttps, evilHttp})

This runs the following block of code twice, once for each randomly generated origin.

Breakdown of the code

Step 4: Send the modified requests

var rr = http.sendRequest( requestResponse.request() .withRemovedHeader("Origin") .withAddedHeader("Origin", origin) );

This sends the request twice, each time with a forged Origin header. If the server doesn't respond, the loop continues to the next origin.

Breakdown of the code

Step 5: Inspect the response headers

var headers = rr.response().headers().toString().toLowerCase(); var creds = headers.contains("access-control-allow-credentials: true"); var reflect = headers.contains("access-control-allow-origin: " + origin.toLowerCase()); var vary = headers.contains("vary: origin");

This examines the response headers to see how the server handled the forged Origin header.

Breakdown of the code

Step 6: Return an issue if the origin is reflected

if (reflect) { var severity = creds ? AuditIssueSeverity.HIGH : AuditIssueSeverity.MEDIUM; var note = vary ? "" : " (missing Vary: Origin)"; return AuditResult.auditResult( AuditIssue.auditIssue( "CORS: arbitrary origin reflection" + note, "Reflected Origin: " + origin + "; credentials=" + creds, "Use strict allowlist; include Vary: Origin.", rr.request().url(), severity, AuditIssueConfidence.FIRM, "", "", severity, rr ) ); }

This creates variables that determine the severity of the finding based on whether the server allows credentials, and adds a note if the Vary: Origin header is missing. It then wraps the issue variables into an AuditIssue and returns an AuditResult containing that issue.

Breakdown of the code

Step 7: Return no issue when the header is present

return AuditResult.auditResult();

If the check didn't detect a reflection or exited early in Step 1, it returns an empty result so no issue is reported.