A web vulnerability scanner plugin for Caido.
Scanner is a vulnerability detection plugin that brings automated security testing capabilities to Caido. Scanner provides a user-friendly interface for identifying common web application vulnerabilities.
- Open Caido, navigate to the
Plugins
sidebar page and then to theCommunity Store
tab - Find
Scanner
and click Install - Done! 🎉
- Go to the Scanner Releases tab and download the latest
plugin_package.zip
file - In your Caido instance, navigate to the
Plugins
page, clickInstall
and select the downloadedplugin_package.zip
file - Done! 🎉
Join our Discord community and connect with other Caido users! Share your ideas, ask questions, and get involved in discussions around Caido and security testing.
The project is organized into distinct packages: backend
, frontend
, engine
, and shared
. The engine
package is responsible for all the logic related to executing checks. All checks are located within the packages/backend/src/checks/
directory.
To define your own check, use the defineCheck
function as shown below. The check metadata contains static information about the check, some fields, such as id
, are always required, while others are used for filtering.
import { defineCheck, Severity } from "engine";
export const exampleCheck = defineCheck(({ step }) => {
return {
metadata: {
id: "example-check",
name: "Example Check",
description:
"This is an example check",
type: "passive",
tags: ["example"],
severities: [Severity.INFO],
aggressivity: {
minRequests: 0,
maxRequests: 0,
},
},
initState: () => ({}),
dedupeKey: (context) => {
return (
context.request.getHost() +
context.request.getPort() +
context.request.getPath()
);
},
when: (context) => {
return (
context.response !== undefined && context.response.getCode() === 200
);
},
};
});
The CheckMetadata
type is a crucial part of defining a check, as it contains all the static information about the check. Here's a breakdown of its components:
- id: A unique identifier for the check. This is required and ensures that each check can be distinctly referenced.
- name: A human-readable name for the check, which is displayed in the UI.
- description: A detailed explanation of what the check does and the vulnerabilities it detects. This helps users understand the check's functionality and scope.
- tags: An array of tags used for categorization and filtering. Tags help in organizing checks and making them easily searchable.
- aggressivity: This defines the request limits for the check. It uses the
CheckAggressivity
type, which specifiesminRequests
andmaxRequests
. If the request count is dynamic, useInfinity
formaxRequests
. - type: Indicates whether the check is
passive
oractive
. This helps in determining how the check interacts with the target. Usepassive
if the scan is silent enough to run in the background without causing noise, andactive
if the scan requires more noticeable interaction with the target. - severities: An array of possible severity levels that the check can report. This is used for filtering, and the engine will throw an error if a finding is returned with a severity not included in this array.
- dependsOn (optional): An array of check IDs that must run before this check. This ensures that dependencies are resolved before execution.
- minAggressivity (optional): The minimum scan aggressivity level required for this check to run. This allows checks to be gated by the scan's aggressivity level.
- skipIfFoundBy (optional): An array of check IDs. If any of these checks have found findings during the scan, this check will be skipped.
export type CheckAggressivity = {
minRequests: number;
maxRequests: number | "Infinity";
};
export type CheckType = "passive" | "active";
export type CheckMetadata = {
/** Unique identifier for the check */
id: string;
/** Human-readable name displayed in the UI */
name: string;
/** Detailed description of what the check does and what vulnerabilities it detects */
description: string;
/** Array of tags used for categorization and filtering */
tags: string[];
/** Defines the request limits for this check. Please use Infinity if it's dynamic. */
aggressivity: CheckAggressivity;
/** Whether this is a passive or active check */
type: CheckType;
/**
* Array of possible severity levels this check can report.
* This is used for filtering.
* Engine will throw an error if you return a finding with a severity that is not in this array.
**/
severities: Severity[];
/** Optional: Array of check IDs that must run before this check */
dependsOn?: string[];
/** Optional: Minimum scan aggressivity level required for this check to run */
minAggressivity?: ScanAggressivity;
/** Optional: array of check IDs - if any of these check IDs have found any findings during the scan, skip this check */
skipIfFoundBy?: string[];
};
Steps let you break your check into smaller parts. Each step should be simple and quick. End a step with done(...)
or go to the next step with continueWith({ state, nextStep: '...' })
.
import { continueWith, defineCheck, done, Severity } from "engine";
export default defineCheck<{
responseBody: string | undefined;
}>(({ step }) => {
step("check200", async (state, context) => {
if (
context.target.response !== undefined &&
context.target.response.getCode() === 200
) {
const responseBody = context.target.response.getBody()?.toText();
return continueWith({
state: {
responseBody,
},
nextStep: "reportFinding",
});
}
return done({ state });
});
step("reportFinding", async (state, context) => {
const finding = {
name: "HTTP 200 OK",
description: `Target responded with 200 OK. Response body: ${state.responseBody}`,
severity: Severity.INFO,
correlation: {
requestID: context.target.request.getId(),
locations: [],
},
};
return done({ state, findings: [finding] });
});
return {
metadata: {
id: "example-check",
name: "Example Check",
description: "This is an example check",
type: "passive",
tags: ["example"],
severities: [Severity.INFO],
aggressivity: {
minRequests: 0,
maxRequests: 0,
},
},
dedupeKey: (context) =>
context.request.getHost() +
context.request.getPort() +
context.request.getPath(),
when: (context) => context.response !== undefined,
initState: () => ({
responseBody: undefined,
}),
};
});
State allows you to pass data from one step to another within your check. When you define a state for your check, you must also provide an initState
function in your return statement that sets the initial value for your state. Context gives you access to the Caido Backend SDK, the scanner engine runtime SDK (which will be explained later), as well as the target request and response.
The checks engine is built around a step-based execution model that allows responsive and interruptible scans. Each check is composed of sequential steps that can pass data through state, send requests, and produce findings.
Steps are the fundamental building blocks of a check in the engine. Steps are executed sequentially, and each execution of a step is called a "tick".
Keeping each tick short is important. After every tick, the engine checks if the scan has been aborted. If a step were to perform multiple long-running actions (like sending several requests in a loop), the user would have to wait for all of them to finish. By designing checks so that each step only sends one request or performs a small piece of logic, we ensure that the engine remains responsive and can quickly react to user actions or scan interruptions.
When you need to perform multiple actions (such as sending several requests), structure your check so that each action happens in its own step. This way, each tick is short, and the engine can efficiently manage scan flow and user interactions.
A common pattern in checks is to create loops by reusing the same step in nextStep
. This allows you to perform iterative operations like testing multiple paths or parameters. Once your loop condition is met, you can either return done()
to complete the check or continue()
to proceed to subsequent steps.
Context provides access to:
- The Caido Backend SDK via the
sdk
field - The request and response of the scan target via the
target
field - The Runtime SDK via the
runtime
field - The scan configuration via the
config
field
The runtime SDK is a set of utilities specifically scoped to the current scan.
/** Runtime SDK for accessing utilities scoped to the current scan. */
runtime: {
/** Utilities for parsing HTML. */
html: {
/** Parse the HTML of a request. This returns a ParsedHtml which has DOM-like methods for querying the HTML. */
parse: (requestID: string) => Promise<ParsedHtml>;
};
/** Access to the dependencies of the check. */
dependencies: {
/** Get a dependency by key. */
get: (key: string) => JSONSerializable | undefined;
};
};
Use dependencies to pass outputs from one check to another.
- Declare upstream check IDs in
metadata.dependsOn
. The engine orders checks so dependencies always finish before dependents run. - Produce data from a check using
output({ state, context })
. - Read dependency values in another check via
context.runtime.dependencies.get('<check-id>')
.
Example: provider check that exposes an output
import { defineCheck, Severity } from "engine";
export const helloWorldProvider = defineCheck(({ step }) => {
return {
metadata: {
id: "example-output",
name: "Example Output Provider",
description: "Produces a simple string output",
type: "passive",
tags: ["example"],
severities: [Severity.INFO],
aggressivity: {
minRequests: 0,
maxRequests: 0,
},
},
// Notice output being used here
output: ({ state, context }) => {
return "Hello world!";
},
initState: () => ({}),
dedupeKey: (context) => {
return (
context.request.getHost() +
context.request.getPort() +
context.request.getPath()
);
},
when: (context) => {
return (
context.response !== undefined && context.response.getCode() === 200
);
},
};
});
Example: consumer check that depends on the provider
import { defineCheck, done, Severity } from "engine";
export const helloWorldConsumer = defineCheck(({ step }) => {
step("example-step", async (state, context) => {
const dependency = context.runtime.dependencies.get(
"example-output"
) as string;
console.log(dependency); // "Hello world!"
return done({ state });
});
return {
metadata: {
id: "example-consumer",
name: "Example Consumer",
description: "Consumes output from example-output",
type: "passive",
tags: ["example"],
severities: [Severity.INFO],
aggressivity: {
minRequests: 0,
maxRequests: 0,
},
// Notice "example-output" being used in dependsOn, engine will always make sure to run it first
dependsOn: ["example-output"],
},
initState: () => ({}),
dedupeKey: (context) => {
return (
context.request.getHost() +
context.request.getPort() +
context.request.getPath()
);
},
when: (context) => {
return (
context.response !== undefined && context.response.getCode() === 200
);
},
};
});
- Outputs are stored per check ID and made available to later checks.
- Outputs must be JSON‑serializable. Keep them small and focused.
The engine provides helper utilities you can use inside checks. Two commonly used ones are redirection detection and URL bypass payload generation.
import { createUrlBypassGenerator, findRedirection } from "engine";
Detects whether a response leads to a redirect and extracts its destination. It covers HTTP 3xx with Location header, Refresh header, HTML meta refresh/location, base tag, and JavaScript-based redirects.
async function findRedirection(
requestID: string,
context: RuntimeContext,
): Promise<RedirectionInfo>
export type RedirectionType =
| "http"
| "meta-refresh"
| "meta-location"
| "refresh-header"
| "base-tag"
| "javascript";
export type RedirectionInfo =
| { hasRedirection: false }
| { hasRedirection: true; type: RedirectionType; location: string };
Generates a sequence of URL payload recipes that attempt to bypass naive allowlists. Each recipe can produce a payload string value and validate that a resulting redirect points to the attacker host. You can filter techniques or limit the number of generated recipes.
function createUrlBypassGenerator(input: {
expectedHost: string;
attackerHost: string;
originalValue?: string;
protocol?: string;
}): UrlBypassGenerator
Filter or cap techniques:
const generator = createUrlBypassGenerator({
expectedHost: "example.com",
attackerHost: "attacker.test",
}).only("UserInfoBypass", "SchemeRelative");
import { defineCheck, done, Severity, createUrlBypassGenerator, findRedirection } from "engine";
export default defineCheck(({ step }) => {
step("testOpenRedirect", async (state, context) => {
const currentParam = "redirect";
const generator = createUrlBypassGenerator({
expectedHost: context.target.request.getHost(),
attackerHost: "attacker.com",
protocol: "https:",
});
for (const payloadRecipe of generator) {
const payload = payloadRecipe.generate();
const params = new URLSearchParams(context.target.request.getQuery());
params.set(currentParam, payload.value);
const spec = context.target.request.toSpec();
spec.setQuery(params.toString());
const { request } = await context.sdk.requests.send(spec);
const redirectInfo = await findRedirection(request.getId(), context);
if (redirectInfo.hasRedirection) {
const redirectUrl = new URL(
redirectInfo.location,
context.target.request.getUrl(),
);
if (payload.validatesWith(redirectUrl)) {
return done({
state,
findings: [
{
name: `Open Redirect in parameter '${currentParam}'`,
description: "Target redirected to an external domain.",
severity: Severity.MEDIUM,
correlation: {
requestID: request.getId(),
locations: [],
},
},
],
});
}
}
}
return done({ state });
});
return {
metadata: {
id: "example-open-redirect",
name: "Example Open Redirect",
description: "Demonstrates using utilities to find open redirects",
type: "active",
tags: ["open-redirect"],
severities: [Severity.MEDIUM],
aggressivity: { minRequests: 0, maxRequests: "Infinity" },
},
initState: () => ({}),
dedupeKey: (context) =>
context.request.getHost() +
context.request.getPort() +
context.request.getPath(),
};
});
Feel free to contribute! If you'd like to request a feature or report a bug, please create a GitHub Issue.
- Report bugs and request features via GitHub Issues.
- Improve docs and examples.
- Add new vulnerability checks.
Note
All checks live in packages/backend/src/checks
. Each check uses a folder with two files: index.ts
and index.spec.ts
. As the logic gets more complicated, you can spread it into multiple files within the same folder.
Create a new folder and add your implementation and tests:
packages/backend/src/checks/
├─ my-check/
│ ├─ index.ts
│ └─ index.spec.ts
Follow the check patterns in the existing checks. Implement your check with the engine step model and add unit tests in index.spec.ts
.
Open packages/backend/src/checks/index.ts
and:
- Add an import for your check
- Add a new entry to
Checks
- Append your check to the
checks
array
import myCheckScan from "./my-check";
export const Checks = {
// ...
MY_CHECK: "my-check",
} as const;
export const checks = [
// ...
myCheckScan,
] as const;
Open packages/backend/src/stores/config.ts
and include your check in default presets. Always add it to the Heavy preset as enabled. Depending on aggressivity and type, you may also add it to Light or Balanced.
Decide whether your check belongs to active
or passive
based on metadata.type
in your check.
{
name: "Heavy",
active: [
// ...
{ checkID: Checks.MY_CHECK, enabled: true },
],
passive: [
// ...
],
}
Run formatting, types, tests, and a build from the repo root:
pnpm lint
pnpm typecheck
pnpm test
pnpm build