This commit is contained in:
imnyang 2025-06-02 22:05:37 +09:00
commit d820695cec
5 changed files with 100 additions and 62 deletions

View file

@ -51,7 +51,7 @@ export class AccessTokenLeakController {
return { return {
found: true, found: true,
location: 'url', location: 'url',
title: "Access Token Leak in URL", title: "Token Leak in URL",
description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`,
value: url value: url
}; };
@ -69,7 +69,7 @@ export class AccessTokenLeakController {
return { return {
found: true, found: true,
location: 'body', location: 'body',
title: "Access Token Leak in Request Body", title: "Token Leak in Request Body",
description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`,
value: bodyText value: bodyText
}; };
@ -98,7 +98,7 @@ export class AccessTokenLeakController {
return { return {
found: true, found: true,
location: 'header', location: 'header',
title: "Access Token Leak in Redirect URL", title: "Token Leak in Redirect URL",
description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`,
value: locationHeaderStr value: locationHeaderStr
}; };
@ -117,7 +117,7 @@ export class AccessTokenLeakController {
return { return {
found: true, found: true,
location: 'body', location: 'body',
title: "Access Token Leak in Response Body", title: "Token Leak in Response Body",
description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`,
value: bodyText value: bodyText
}; };
@ -136,10 +136,18 @@ private extractTokenFromText(text: string): string | null {
// 토큰 관련 키워드 리스트 // 토큰 관련 키워드 리스트
const tokenKeys = [ const tokenKeys = [
'access_token', 'access_token',
'id_token', 'accesstoken',
'Access-Token',
'Refresh_Token',
'Refresh-Token',
'RefreshToken',
'Secret_Token',
'Secret-Token',
'SecretToken',
'SSO_Auth',
'SSO-Auth',
'SSOAuth',
'auth_token', 'auth_token',
'token',
'jwt',
'session_token' 'session_token'
]; ];

View file

@ -5,18 +5,27 @@ import { HttpUtils } from "../utils/http";
const httpUtils = new HttpUtils(); const httpUtils = new HttpUtils();
export class CsrfCheck { export class CsrfCheck {
private isTargetUri(uri: string): boolean {
if (
uri.includes("client_id=") &&
(uri.includes("response_type=") ||
uri.includes("grant_type=") ||
uri.includes("redirect_uri=") ||
uri.includes("scope=") ||
uri.includes("state=") ||
uri.includes("nonce="))
) {
return true;
}
return false;
}
private isOauthUri(request: Request): boolean { private isOauthUri(request: Request): boolean {
const query = request.getQuery() || ""; const uri = request.getUrl() || "";
// Check if the request is an OAuth authorization request // Check if the request is an OAuth authorization request
if ( if (this.isTargetUri(uri)) {
query.includes("client_id=") &&
(query.includes("response_type=") ||
query.includes("grant_type=") ||
query.includes("redirect_uri=") ||
query.includes("scope=") ||
query.includes("state="))
) {
return true; return true;
} }
@ -25,23 +34,10 @@ export class CsrfCheck {
private isOauthRedirectResponse(response: Response): boolean { private isOauthRedirectResponse(response: Response): boolean {
const status = response.getCode(); const status = response.getCode();
const locationHeader = httpUtils.getHeaderValue( const uri =
response.getHeaders(), httpUtils.getHeaderValue(response.getHeaders(), "location") || "";
"location"
);
if ( if (status >= 300 && status < 400 && this.isTargetUri(uri)) {
status >= 300 &&
status < 400 &&
locationHeader &&
(locationHeader.includes("client_id=") ||
locationHeader.includes("response_type=") ||
locationHeader.includes("grant_type=") ||
locationHeader.includes("redirect_uri=") ||
locationHeader.includes("scope=") ||
locationHeader.includes("state=") ||
locationHeader.includes("code=")) // code is also common in OAuth redirects
) {
return true; return true;
} }
return false; return false;
@ -49,7 +45,9 @@ export class CsrfCheck {
private isStateInQuery(request: Request): boolean { private isStateInQuery(request: Request): boolean {
const query = request.getQuery(); const query = request.getQuery();
const stateValue = httpUtils.getQueryParam(query || "", "state"); const stateValue =
httpUtils.getQueryParam(query || "", "state") ||
httpUtils.getQueryParam(query || "", "nonce");
if (!stateValue) { if (!stateValue) {
return false; return false;
} }
@ -72,17 +70,18 @@ export class CsrfCheck {
// 요청에서 보낸 state 추출 // 요청에서 보낸 state 추출
const query = request.getQuery() || ""; const query = request.getQuery() || "";
const originalState = httpUtils.getQueryParam(query, "state"); const originalState =
httpUtils.getQueryParam(query, "state") ||
httpUtils.getQueryParam(query || "", "nonce");
// 리다이렉트 URL에서 쿼리 부분만 추출 // 리다이렉트 URL에서 쿼리 부분만 추출
const locationHeader = httpUtils.getHeaderValue( const locationHeader = httpUtils.getHeaderValue(
response.getHeaders(), response.getHeaders(),
"location" "location"
); );
const responseState = httpUtils.getQueryParamFromURI( const responseState =
locationHeader || "", httpUtils.getQueryParamFromURI(locationHeader || "", "state") ||
"state" httpUtils.getQueryParamFromURI(locationHeader || "", "nonce");
);
// state가 없거나, 요청값과 다르면 CSRF 위험 // state가 없거나, 요청값과 다르면 CSRF 위험
if (!responseState) { if (!responseState) {

View file

@ -1,4 +1,4 @@
import type { Request } from "caido:utils"; import type { Request, Response } from "caido:utils";
import { TokenLeakCheck } from "./tokenLeakCheck"; import { TokenLeakCheck } from "./tokenLeakCheck";
export class NonceCheckController{ export class NonceCheckController{
@ -6,8 +6,8 @@ export class NonceCheckController{
* OIDC(OpenID Connect) * OIDC(OpenID Connect)
*/ */
public static isOidcFlow(req: Request): boolean { public static isOidcFlow(req: Request, res:Response): boolean {
if(TokenLeakCheck.extractIdToken(req)) { if(TokenLeakCheck.extractIdToken(req, res)) {
return true; return true;
} }
return false; return false;
@ -15,10 +15,10 @@ export class NonceCheckController{
public static isNonceCheckRequest(req: Request): boolean { public static isNonceCheckRequest(req: Request): boolean {
const id_token = decodeIdToken(req); const id_token = TokenLeakCheck.decodeIdToken(req);
// 1. nonce 파라미터가 포함된 요청인지 확인 // 1. nonce 파라미터가 포함된 요청인지 확인
if (id_token.includes("nonce=")) { if (id_token && id_token.includes("nonce=")) {
return true; return true;
} }
@ -26,8 +26,4 @@ export class NonceCheckController{
} }
} }
function decodeIdToken(req: Request): string {
// Implement actual decoding logic here. For now, return an empty string or mock value.
return "";
}

View file

@ -1,8 +1,8 @@
import type { Request } from "caido:utils"; import type { Request,Response } from "caido:utils";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
export class TokenLeakCheck { export class TokenLeakCheck {
public static extractIdToken(req: Request): string | null { public static extractIdToken(req: Request, res?: Response): string | null {
// 1. Authorization 헤더 확인\\ // 1. Authorization 헤더 확인\\
const header = req.getHeaders() as Record<string, string | string[] | undefined>; const header = req.getHeaders() as Record<string, string | string[] | undefined>;
const authHeader = header["authorization"] || header["Authorization"]; const authHeader = header["authorization"] || header["Authorization"];
@ -16,19 +16,21 @@ export class TokenLeakCheck {
return (query as Record<string, any>).id_token; return (query as Record<string, any>).id_token;
} }
// 3. POST 바디 안에 id_token이 있을 경우 // 3. response 안에 id_token이 있을 경우
const rawBody = req.getRaw(); if (res) {
const body = rawBody ? rawBody.toString() : ""; const rawBody = res.getRaw();
const match = body.match(/id_token=([^&\s]+)/); const body = rawBody ? rawBody.toString() : "";
if (match && typeof match[1] === "string") { const match = body.match(/id_token=([^&\s]+)/);
return decodeURIComponent(match[1]); if (match && typeof match[1] === "string" ) {
return decodeURIComponent(match[1]);
}
} }
return null; return null;
} }
public static decodeIdToken(req: Request): Record<string, any> | null { public static decodeIdToken(req: Request, res?: Response): Record<string, any> | null {
const token = this.extractIdToken(req); const token = this.extractIdToken(req, res);
if (!token) return null; if (!token) return null;
const decoded = jwt.decode(token, { complete: true }); const decoded = jwt.decode(token, { complete: true });

View file

@ -6,6 +6,7 @@ import { CsrfCheck } from "./controller/csrfCheck";
import { PKCECheck } from "./controller/PKCECheck"; import { PKCECheck } from "./controller/PKCECheck";
import { AccessTokenLeakController } from "./controller/accessTokenDetector"; import { AccessTokenLeakController } from "./controller/accessTokenDetector";
import { ScopeDetection } from "./controller/scopeDetection"; import { ScopeDetection } from "./controller/scopeDetection";
import { NonceCheckController } from "./controller/nonceCheck";
export type API = DefineAPI<{}>; export type API = DefineAPI<{}>;
@ -15,10 +16,15 @@ const csrfCheck = new CsrfCheck();
const pkceCheckController = new PKCECheck(); const pkceCheckController = new PKCECheck();
const tokenCheck = new AccessTokenLeakController(); const tokenCheck = new AccessTokenLeakController();
const ScopeDetectionController = new ScopeDetection(); const ScopeDetectionController = new ScopeDetection();
const nonceCheckController = new NonceCheckController();
export function init(sdk: SDK<API>) { export function init(sdk: SDK<API>) {
// sdk.events.onInterceptRequest(async (sdk, req: Request) => { sdk.events.onInterceptResponse(async (sdk, req: Request, res: Response) => {
// const result = csrfCheck.checker(req); await csrfCheck.checker(sdk, req, res);
await pkceCheckController.test(sdk, req);
await tokenCheck.testReq(sdk, req);
await tokenCheck.testResp(sdk, res, req);
await ScopeDetectionController.scan(sdk, req.getUrl());
// if (result) { // if (result) {
// await sdk.findings.create({ // await sdk.findings.create({
@ -54,6 +60,33 @@ export function init(sdk: SDK<API>) {
// request: req, // request: req,
// reporter: "", // reporter: "",
// }); // });
} if (NonceCheckController.isOidcFlow(req, res)) {
); await sdk.findings.create({
} title: "OIDC Flow Detected",
description: "The request appears to be part of an OIDC flow.",
request: req,
reporter: "",
});
}
});
/*
sdk.events.onInterceptRequest(async (sdk, req: Request) => {
const result =
authZCodeGrantController.testReq(req) ||
implicitGrantController.testReq(req);
if (result) {
await pkceCheckController.test(sdk, req);
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: "",
});
}
});
*/
}
)}