190 lines
7.2 KiB
JavaScript
190 lines
7.2 KiB
JavaScript
// packages/backend/src/controller/PKCECheck.ts
|
|
import { RequestSpec } from "caido:utils";
|
|
var PKCECheck = class {
|
|
async test(sdk, req) {
|
|
const method = req.getMethod();
|
|
if (method !== "GET") {
|
|
sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping.");
|
|
return false;
|
|
}
|
|
const query = req.getQuery();
|
|
const searchParams = new URLSearchParams(query);
|
|
const requiredKeys = ["client_id", "response_type", "code_challenge", "code_challenge_method"];
|
|
if (!requiredKeys.every((key) => searchParams.has(key))) {
|
|
sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping.");
|
|
return false;
|
|
}
|
|
const url = req.getUrl();
|
|
const isOpenID = searchParams.get("scope")?.includes("openid") || url.includes("id_token");
|
|
const methodVal = searchParams.get("code_challenge_method");
|
|
const challengeVal = searchParams.get("code_challenge");
|
|
if (!methodVal || !challengeVal) {
|
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping.");
|
|
await sdk.findings.create({
|
|
title: isOpenID ? "[WARN] OpenID Flow PKCE Parameters Missing" : "[WARN] OAuth2 Flow PKCE Parameters Missing",
|
|
description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`,
|
|
request: req,
|
|
reporter: "PKCE Checker"
|
|
});
|
|
return false;
|
|
}
|
|
if (methodVal === "plain") {
|
|
sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping.");
|
|
await sdk.findings.create({
|
|
title: isOpenID ? "[WARN] OpenID Flow PKCE Method is 'plain'" : "[WARN] OAuth2 Flow PKCE Method is 'plain'",
|
|
description: `PKCE method is set to 'plain' for ${url}. This may indicate a downgrade vulnerability.`,
|
|
request: req,
|
|
reporter: "PKCE Checker"
|
|
});
|
|
return false;
|
|
}
|
|
searchParams.delete("code_challenge");
|
|
searchParams.delete("code_challenge_method");
|
|
const downgradedQuery = searchParams.toString();
|
|
const scheme = req.getUrl().startsWith("https") ? "https" : "http";
|
|
const downgradedUrl = `${scheme}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`;
|
|
sdk.console.log(`${req.getHost()} Original URL: ` + url);
|
|
sdk.console.log(`${req.getHost()} Downgraded URL: ` + downgradedUrl);
|
|
try {
|
|
const spec = new RequestSpec(downgradedUrl);
|
|
spec.setBody(req.getBody());
|
|
for (const [key, value] of Object.entries(req.getHeaders())) {
|
|
if (Array.isArray(value)) {
|
|
spec.setHeader(key, value.join(", "));
|
|
} else {
|
|
spec.setHeader(key, value);
|
|
}
|
|
}
|
|
spec.setHost(req.getHost());
|
|
spec.setMethod(req.getMethod());
|
|
spec.setPath(req.getPath());
|
|
spec.setQuery(downgradedQuery);
|
|
spec.setTls(req.getTls());
|
|
spec.setPort(req.getPort());
|
|
let sendDowngradedRequest = await sdk.requests.send(spec);
|
|
if (sendDowngradedRequest.response) {
|
|
let domain = spec.getHost();
|
|
let port = spec.getPort();
|
|
let path2 = spec.getPath();
|
|
let query2 = spec.getQuery();
|
|
let id = sendDowngradedRequest.response.getId();
|
|
let code = sendDowngradedRequest.response.getCode();
|
|
sdk.console.log(`REQ ${id}: ${domain}:${port}${path2}${query2} received a status code of ${code}`);
|
|
}
|
|
if (sendDowngradedRequest.response?.getCode() === 302) {
|
|
await sdk.findings.create({
|
|
title: isOpenID ? "[CRITICAL] OpenID Flow PKCE Downgrade Vulnerability" : "[CRITICAL] OAuth2 Flow PKCE Downgrade Vulnerability",
|
|
description: `The request to ${url} is vulnerable to a PKCE downgrade attack. This may indicate a configuration error.`,
|
|
request: req,
|
|
reporter: "PKCE Checker"
|
|
});
|
|
}
|
|
} catch (err) {
|
|
sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`);
|
|
}
|
|
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// packages/backend/src/controller/redirectUriCheck.ts
|
|
import { promises as fs } from "fs";
|
|
import * as path from "path";
|
|
import os from "os";
|
|
var redirectUriCheck = class {
|
|
requestMap = /* @__PURE__ */ new Map();
|
|
// constructor(private sdk: SDK<any>) {}
|
|
async onRequest(sdk, req) {
|
|
try {
|
|
const urlString = req.getUrl();
|
|
const url = new URL(urlString);
|
|
sdk.console.log(`[OAuthPlugin] Intercepted request: ${urlString}`);
|
|
if (!url.pathname.includes("/authorize") && !url.pathname.includes("/auth")) return;
|
|
const params = new URLSearchParams(url.search);
|
|
const redirectUri = params.get("redirect_uri");
|
|
if (!redirectUri) return;
|
|
const reqId = req.getId();
|
|
this.requestMap.set(reqId, redirectUri);
|
|
const clientId = params.get("client_id") ?? "(missing)";
|
|
const responseType = params.get("response_type") ?? "(missing)";
|
|
const isScan = params.has("scan");
|
|
if (isScan) return;
|
|
const output = {
|
|
original_url: urlString,
|
|
client_id: clientId,
|
|
redirect_uri: redirectUri,
|
|
response_type: responseType
|
|
};
|
|
try {
|
|
const filePath = path.join(os.tmpdir(), "oauth-fuzz-input.json");
|
|
await fs.writeFile(filePath, JSON.stringify(output, null, 2));
|
|
} catch (err) {
|
|
await sdk.findings.create({
|
|
title: "[fs] Write Failed",
|
|
description: `Could not write to file: ${err}`,
|
|
request: req,
|
|
reporter: "oauth-open-redirect-detector"
|
|
});
|
|
}
|
|
await sdk.findings.create({
|
|
title: "[ ] OAuth2 Authorization Request Collected",
|
|
description: `client_id: ${clientId}
|
|
redirect_uri: ${redirectUri}
|
|
response_type: ${responseType}`,
|
|
request: req,
|
|
reporter: "oauth-open-redirect-detector"
|
|
});
|
|
} catch (err) {
|
|
sdk.console.error(`Error in onRequest: ${err}`);
|
|
}
|
|
}
|
|
async onResponse(sdk, req, resp) {
|
|
try {
|
|
const reqId = req.getId();
|
|
const url = new URL(req.getUrl());
|
|
const status = resp.getCode();
|
|
const location = resp.getHeader("location")?.[0];
|
|
const params = new URLSearchParams(url.search);
|
|
const isScan = params.has("scan");
|
|
if (!isScan) {
|
|
this.requestMap.delete(reqId);
|
|
return;
|
|
}
|
|
if (status >= 300 && status < 400 && location) {
|
|
const redirectUri = this.requestMap.get(reqId) ?? "(unknown)";
|
|
await sdk.findings.create({
|
|
title: "[+] Redirect URI Misconfiguration Detected",
|
|
description: `Status: ${status}
|
|
Location: ${location}
|
|
Request URL: ${url.href}
|
|
Redirect URI: ${redirectUri}`,
|
|
request: req,
|
|
reporter: "oauth-open-redirect-detector"
|
|
});
|
|
}
|
|
this.requestMap.delete(reqId);
|
|
} catch (err) {
|
|
sdk.console.error(`Error in onResponse: ${err}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
// packages/backend/src/index.ts
|
|
var pkceCheckController = new PKCECheck();
|
|
var redirectUriCheckController = new redirectUriCheck();
|
|
function init(sdk) {
|
|
sdk.events.onInterceptRequest(
|
|
async (sdk2, req) => {
|
|
await redirectUriCheckController.onRequest(sdk2, req);
|
|
}
|
|
);
|
|
sdk.events.onInterceptResponse(
|
|
async (sdk2, req, resp) => {
|
|
await pkceCheckController.test(sdk2, req);
|
|
await redirectUriCheckController.onResponse(sdk2, req, resp);
|
|
}
|
|
);
|
|
}
|
|
export {
|
|
init
|
|
};
|