[add] redirectUriCheckController
This commit is contained in:
parent
d35af82aae
commit
9c4b53a6bc
27 changed files with 1235 additions and 161 deletions
145
packages/backend/dist/index.js
vendored
145
packages/backend/dist/index.js
vendored
|
|
@ -1,20 +1,110 @@
|
|||
// packages/backend/src/index.ts
|
||||
// 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 requestMap = /* @__PURE__ */ new Map();
|
||||
function init(sdk) {
|
||||
sdk.events.onInterceptRequest(async (sdk2, req) => {
|
||||
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);
|
||||
sdk2.console.log(`[OAuthPlugin] Intercepted request: ${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();
|
||||
requestMap.set(reqId, redirectUri);
|
||||
this.requestMap.set(reqId, redirectUri);
|
||||
const clientId = params.get("client_id") ?? "(missing)";
|
||||
const responseType = params.get("response_type") ?? "(missing)";
|
||||
const isScan = params.has("scan");
|
||||
|
|
@ -29,14 +119,14 @@ function init(sdk) {
|
|||
const filePath = path.join(os.tmpdir(), "oauth-fuzz-input.json");
|
||||
await fs.writeFile(filePath, JSON.stringify(output, null, 2));
|
||||
} catch (err) {
|
||||
await sdk2.findings.create({
|
||||
await sdk.findings.create({
|
||||
title: "[fs] Write Failed",
|
||||
description: `Could not write to file: ${err}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector"
|
||||
});
|
||||
}
|
||||
await sdk2.findings.create({
|
||||
await sdk.findings.create({
|
||||
title: "[ ] OAuth2 Authorization Request Collected",
|
||||
description: `client_id: ${clientId}
|
||||
redirect_uri: ${redirectUri}
|
||||
|
|
@ -45,10 +135,10 @@ response_type: ${responseType}`,
|
|||
reporter: "oauth-open-redirect-detector"
|
||||
});
|
||||
} catch (err) {
|
||||
sdk2.console.error(`Error in onInterceptRequest: ${err}`);
|
||||
sdk.console.error(`Error in onRequest: ${err}`);
|
||||
}
|
||||
});
|
||||
sdk.events.onInterceptResponse(async (sdk2, req, resp) => {
|
||||
}
|
||||
async onResponse(sdk, req, resp) {
|
||||
try {
|
||||
const reqId = req.getId();
|
||||
const url = new URL(req.getUrl());
|
||||
|
|
@ -57,26 +147,43 @@ response_type: ${responseType}`,
|
|||
const params = new URLSearchParams(url.search);
|
||||
const isScan = params.has("scan");
|
||||
if (!isScan) {
|
||||
requestMap.delete(reqId);
|
||||
this.requestMap.delete(reqId);
|
||||
return;
|
||||
}
|
||||
if (status >= 300 && status < 400 && location) {
|
||||
const redirectUri = requestMap.get(reqId) ?? "(unknown)";
|
||||
await sdk2.findings.create({
|
||||
const redirectUri = this.requestMap.get(reqId) ?? "(unknown)";
|
||||
await sdk.findings.create({
|
||||
title: "[+] Redirect URI Misconfiguration Detected",
|
||||
description: `Status: ${status}
|
||||
Location: ${location}
|
||||
Original Redirect URI: ${redirectUri}
|
||||
Request URL: ${url.href}`,
|
||||
Request URL: ${url.href}
|
||||
Redirect URI: ${redirectUri}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector"
|
||||
});
|
||||
}
|
||||
requestMap.delete(reqId);
|
||||
this.requestMap.delete(reqId);
|
||||
} catch (err) {
|
||||
sdk2.console.error(`Error in onInterceptResponse: ${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
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"build": "vite build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
144
packages/backend/src/controller/PKCECheck.ts
Normal file
144
packages/backend/src/controller/PKCECheck.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import type { SDK } from "caido:plugin";
|
||||
import { Body, RequestSpec, type Request } from "caido:utils";
|
||||
|
||||
export class PKCECheck {
|
||||
async test(sdk: SDK, req: Request): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
// Remove PKCE parameters to simulate a downgraded request
|
||||
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 {
|
||||
// Use Caido Replay SDK to replay the original request
|
||||
const spec = new RequestSpec(downgradedUrl);
|
||||
spec.setBody(req.getBody() as Body);
|
||||
for (const [key, value] of Object.entries(req.getHeaders())) {
|
||||
if (Array.isArray(value)) {
|
||||
spec.setHeader(key, value.join(', ')); // or another suitable delimiter
|
||||
} 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 path = spec.getPath();
|
||||
let query = spec.getQuery();
|
||||
let id = sendDowngradedRequest.response.getId();
|
||||
let code = sendDowngradedRequest.response.getCode();
|
||||
sdk.console.log(`REQ ${id}: ${domain}:${port}${path}${query} 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",
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
sdk.console.log(`${req.getHost()} Original Status: ` + resOriginal.status);
|
||||
sdk.console.log(`${req.getHost()} Downgraded Status: ` + resDowngraded.status);
|
||||
|
||||
sdk.console.log(`${req.getHost()} Original Headers: ` + JSON.stringify(resOriginal.headers));
|
||||
sdk.console.log(`${req.getHost()} Downgraded Headers: ` + JSON.stringify(resDowngraded.headers));
|
||||
|
||||
// Caido Dev Docs 기준으로, 리다이렉트된 URL은 Response 객체의 url 속성에 저장되어 있음
|
||||
const locationOriginal = resOriginal.url ?? "";
|
||||
const locationDowngraded = resDowngraded.url ?? "";
|
||||
|
||||
sdk.console.log(`${req.getHost()} Original Location: ` + locationOriginal);
|
||||
sdk.console.log(`${req.getHost()} Downgraded Location: ` + locationDowngraded);
|
||||
|
||||
const statusEqual = resOriginal.status === resDowngraded.status;
|
||||
const codeInRedirects = locationOriginal.includes("code=") && locationDowngraded.includes("code=");
|
||||
|
||||
if (statusEqual && codeInRedirects) {
|
||||
const title = isOpenID
|
||||
? "[CRITICAL] OpenID Flow PKCE Downgraded to Plaintext"
|
||||
: "[CRITICAL] OAuth2 Flow PKCE Downgraded to Plaintext";
|
||||
const reference = isOpenID
|
||||
? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7"
|
||||
: "https://datatracker.ietf.org/doc/html/rfc7636";
|
||||
|
||||
await sdk.findings.create({
|
||||
title,
|
||||
description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nRedirect contained code=.\n\nReference: ${reference}`,
|
||||
request: req,
|
||||
reporter: "",
|
||||
});
|
||||
|
||||
return true;
|
||||
}*/
|
||||
} catch (err) {
|
||||
sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`);
|
||||
}
|
||||
|
||||
sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
48
packages/backend/src/controller/authZCodeGrant.ts
Normal file
48
packages/backend/src/controller/authZCodeGrant.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { Request } from "caido:utils";
|
||||
|
||||
export class AuthZCodeGrantController {
|
||||
constructor() {}
|
||||
|
||||
isAuthZReq(req: Request) {
|
||||
// const path = req.getPath();
|
||||
const query = req.getQuery();
|
||||
// const raw = req.getRaw().toString();
|
||||
|
||||
// 쿼리에 client_id and response_type=code 포함 확인
|
||||
if (query.includes("client_id=") && query.includes("response_type=code")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isSendCodeToClient(req: Request) {
|
||||
const path = req.getPath();
|
||||
const query = req.getQuery();
|
||||
|
||||
// code & state 쌍 또는 path에 &code= 또는 쿼리로 code= 유사 일치 확인
|
||||
if (
|
||||
(query.includes("code=") && query.includes("state=")) ||
|
||||
path.includes("&code=") ||
|
||||
/code=%/i.test(query)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
testReq(req: Request) {
|
||||
if (this.isAuthZReq(req)) {
|
||||
return "isAuthZReq";
|
||||
}
|
||||
if (this.isSendCodeToClient(req)) {
|
||||
return "isSendCodeToClient";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// isAccessTokenReq(req: Response) {
|
||||
|
||||
// }
|
||||
}
|
||||
40
packages/backend/src/controller/implictGrant.ts
Normal file
40
packages/backend/src/controller/implictGrant.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Request } from "caido:utils";
|
||||
|
||||
export class ImplicitGrantController {
|
||||
isImplicitGrantReq(req: Request) {
|
||||
const query = req.getQuery();
|
||||
// const raw = req.getRaw().toString();
|
||||
|
||||
// 쿼리에 client_id and response_type=token 포함 확인
|
||||
if (query.includes("client_id=") && query.includes("response_type=token")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isSendTokenToClient(req: Request) {
|
||||
const path = req.getPath();
|
||||
const query = req.getQuery();
|
||||
|
||||
if (
|
||||
(query.includes("access_token=") && query.includes("state=")) ||
|
||||
path.includes("&access_token=") ||
|
||||
/access_token=%/i.test(query)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
testReq(req: Request) {
|
||||
if (this.isImplicitGrantReq(req)) {
|
||||
return "isImplicitGrantReq";
|
||||
}
|
||||
if (this.isSendTokenToClient(req)) {
|
||||
return "isSendTokenToClient";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
33
packages/backend/src/controller/nonceCheck.ts
Normal file
33
packages/backend/src/controller/nonceCheck.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { Request } from "caido:utils";
|
||||
import { TokenLeakCheck } from "./tokenLeakCheck";
|
||||
|
||||
export class NonceCheckController{
|
||||
/**
|
||||
* 응답이 OIDC(OpenID Connect) 플로우인지 확인하는 메서드
|
||||
*/
|
||||
|
||||
public static isOidcFlow(req: Request): boolean {
|
||||
if(TokenLeakCheck.extractIdToken(req)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static isNonceCheckRequest(req: Request): boolean {
|
||||
const id_token = decodeIdToken(req);
|
||||
|
||||
// 1. nonce 파라미터가 포함된 요청인지 확인
|
||||
if (id_token.includes("nonce=")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeIdToken(req: Request): string {
|
||||
// Implement actual decoding logic here. For now, return an empty string or mock value.
|
||||
return "";
|
||||
}
|
||||
|
||||
95
packages/backend/src/controller/redirectUriCheck.ts
Normal file
95
packages/backend/src/controller/redirectUriCheck.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// oauth-plugin.ts
|
||||
import type { SDK } from "caido:plugin";
|
||||
import type { Request, Response } from "caido:utils";
|
||||
import { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
import os from "os";
|
||||
|
||||
export class redirectUriCheck {
|
||||
private requestMap = new Map<string, string>();
|
||||
|
||||
// constructor(private sdk: SDK<any>) {}
|
||||
|
||||
public async onRequest(sdk: SDK, req: Request) {
|
||||
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}\nredirect_uri: ${redirectUri}\nresponse_type: ${responseType}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
sdk.console.error(`Error in onRequest: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async onResponse(sdk: SDK, req: Request, resp: Response) {
|
||||
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}\nLocation: ${location}\nRequest URL: ${url.href}\nRedirect URI: ${redirectUri}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector",
|
||||
});
|
||||
}
|
||||
|
||||
this.requestMap.delete(reqId);
|
||||
} catch (err) {
|
||||
sdk.console.error(`Error in onResponse: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
packages/backend/src/controller/tokenLeakCheck.ts
Normal file
42
packages/backend/src/controller/tokenLeakCheck.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { Request } from "caido:utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
export class TokenLeakCheck {
|
||||
public static extractIdToken(req: Request): string | null {
|
||||
// 1. Authorization 헤더 확인\\
|
||||
const header = req.getHeaders() as Record<string, string | string[] | undefined>;
|
||||
const authHeader = header["authorization"] || header["Authorization"];
|
||||
if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7).trim();
|
||||
}
|
||||
|
||||
// 2. 쿼리 파라미터
|
||||
const query = req.getQuery();
|
||||
if (query && typeof query === "object" && "id_token" in query) {
|
||||
return (query as Record<string, any>).id_token;
|
||||
}
|
||||
|
||||
// 3. POST 바디 안에 id_token이 있을 경우
|
||||
const rawBody = req.getRaw();
|
||||
const body = rawBody ? rawBody.toString() : "";
|
||||
const match = body.match(/id_token=([^&\s]+)/);
|
||||
if (match && typeof match[1] === "string") {
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static decodeIdToken(req: Request): Record<string, any> | null {
|
||||
const token = this.extractIdToken(req);
|
||||
if (!token) return null;
|
||||
|
||||
const decoded = jwt.decode(token, { complete: true });
|
||||
if (!decoded || typeof decoded !== "object") return null;
|
||||
|
||||
return {
|
||||
header: decoded.header,
|
||||
payload: decoded.payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +1,55 @@
|
|||
import type { SDK, DefineAPI } from "caido:plugin";
|
||||
import type { Request, Response } from "caido:utils";
|
||||
import { promises as fs } from "fs";
|
||||
import * as path from "path";
|
||||
import os from "os";
|
||||
// import { ImplicitGrantController } from "./controller/implictGrant";
|
||||
// import { AuthZCodeGrantController } from "./controller/authZCodeGrant";
|
||||
import { PKCECheck } from "./controller/PKCECheck";
|
||||
import { redirectUriCheck } from "./controller/redirectUriCheck";
|
||||
|
||||
export type API = DefineAPI<{}>;
|
||||
|
||||
const requestMap = new Map<string, string>();
|
||||
// const implicitGrantController = new ImplicitGrantController();
|
||||
// const authZCodeGrantController = new AuthZCodeGrantController();
|
||||
const pkceCheckController = new PKCECheck();
|
||||
const redirectUriCheckController = new redirectUriCheck();
|
||||
|
||||
export function init(sdk: SDK<API>) {
|
||||
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
// const result = csrfCheck.checker(req);
|
||||
|
||||
sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
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();
|
||||
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}\nredirect_uri: ${redirectUri}\nresponse_type: ${responseType}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
sdk.console.error(`Error in onInterceptRequest: ${err}`);
|
||||
// if (result) {
|
||||
// await sdk.findings.create({
|
||||
// title: "Possible SSO Request Detected",
|
||||
// description: `SSO-related parameters detected in request:\n\n${req.getMethod()} ${req.getUrl()} : ${result}`,
|
||||
// request: req,
|
||||
// reporter: "",
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
sdk.events.onInterceptRequest(
|
||||
async(sdk: SDK<DefineAPI<{}>, {}>, req: Request) => {
|
||||
await redirectUriCheckController.onRequest(sdk, req);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
sdk.events.onInterceptResponse(async (sdk, req: Request, resp: Response) => {
|
||||
try {
|
||||
const reqId = req.getId();
|
||||
const url = new URL(req.getUrl());
|
||||
const status = resp.getCode();
|
||||
const location = resp.getHeader("location")?.[0];
|
||||
sdk.events.onInterceptResponse(
|
||||
async (sdk: SDK<DefineAPI<{}>, {}>, req: Request, resp: Response) => {
|
||||
await pkceCheckController.test(sdk, req);
|
||||
await redirectUriCheckController.onResponse(sdk, req, resp);
|
||||
|
||||
const params = new URLSearchParams(url.search);
|
||||
const isScan = params.has("scan");
|
||||
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
// const result =
|
||||
// authZCodeGrantController.testReq(req) ||
|
||||
// implicitGrantController.testReq(req);
|
||||
|
||||
if (!isScan) {
|
||||
requestMap.delete(reqId);
|
||||
return;
|
||||
}
|
||||
// if (result) {
|
||||
// await pkceCheckController.test(sdk, req);
|
||||
|
||||
if (status >= 300 && status < 400 && location) {
|
||||
const redirectUri = requestMap.get(reqId) ?? "(unknown)";
|
||||
|
||||
await sdk.findings.create({
|
||||
title: "[+] Redirect URI Misconfiguration Detected",
|
||||
description: `Status: ${status}\nLocation: ${location}\nOriginal Redirect URI: ${redirectUri}\nRequest URL: ${url.href}`,
|
||||
request: req,
|
||||
reporter: "oauth-open-redirect-detector",
|
||||
});
|
||||
}
|
||||
|
||||
requestMap.delete(reqId);
|
||||
} catch (err) {
|
||||
sdk.console.error(`Error in onInterceptResponse: ${err}`);
|
||||
// await sdk.findings.create({
|
||||
// title: "Possible SSO Request Detected",
|
||||
// description: `SSO-related parameters detected in request:\n\n${req.getMethod()} ${req.getUrl()} : ${result}`,
|
||||
// request: req,
|
||||
// reporter: "",
|
||||
// });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -5,4 +5,4 @@
|
|||
"lib": ["ESNext", "DOM"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue