diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts index 8093a54..6e95120 100644 --- a/packages/backend/src/controller/accessTokenDetector.ts +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -51,7 +51,7 @@ export class AccessTokenLeakController { return { found: true, location: 'url', - title: "Access Token Leak in URL", + title: "Token Leak in URL", description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, value: url }; @@ -69,7 +69,7 @@ export class AccessTokenLeakController { return { found: true, location: 'body', - title: "Access Token Leak in Request Body", + title: "Token Leak in Request Body", description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; @@ -98,7 +98,7 @@ export class AccessTokenLeakController { return { found: true, location: 'header', - title: "Access Token Leak in Redirect URL", + title: "Token Leak in Redirect URL", description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, value: locationHeaderStr }; @@ -117,7 +117,7 @@ export class AccessTokenLeakController { return { found: true, location: 'body', - title: "Access Token Leak in Response Body", + title: "Token Leak in Response Body", description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; @@ -136,10 +136,18 @@ private extractTokenFromText(text: string): string | null { // 토큰 관련 키워드 리스트 const tokenKeys = [ 'access_token', - 'id_token', + 'accesstoken', + 'Access-Token', + 'Refresh_Token', + 'Refresh-Token', + 'RefreshToken', + 'Secret_Token', + 'Secret-Token', + 'SecretToken', + 'SSO_Auth', + 'SSO-Auth', + 'SSOAuth', 'auth_token', - 'token', - 'jwt', 'session_token' ]; diff --git a/packages/backend/src/controller/csrfCheck.ts b/packages/backend/src/controller/csrfCheck.ts index 727b65b..f5018d5 100644 --- a/packages/backend/src/controller/csrfCheck.ts +++ b/packages/backend/src/controller/csrfCheck.ts @@ -5,18 +5,27 @@ import { HttpUtils } from "../utils/http"; const httpUtils = new HttpUtils(); 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 { - const query = request.getQuery() || ""; + const uri = request.getUrl() || ""; // Check if the request is an OAuth authorization request - if ( - query.includes("client_id=") && - (query.includes("response_type=") || - query.includes("grant_type=") || - query.includes("redirect_uri=") || - query.includes("scope=") || - query.includes("state=")) - ) { + if (this.isTargetUri(uri)) { return true; } @@ -25,23 +34,10 @@ export class CsrfCheck { private isOauthRedirectResponse(response: Response): boolean { const status = response.getCode(); - const locationHeader = httpUtils.getHeaderValue( - response.getHeaders(), - "location" - ); + const uri = + httpUtils.getHeaderValue(response.getHeaders(), "location") || ""; - if ( - 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 - ) { + if (status >= 300 && status < 400 && this.isTargetUri(uri)) { return true; } return false; @@ -49,7 +45,9 @@ export class CsrfCheck { private isStateInQuery(request: Request): boolean { const query = request.getQuery(); - const stateValue = httpUtils.getQueryParam(query || "", "state"); + const stateValue = + httpUtils.getQueryParam(query || "", "state") || + httpUtils.getQueryParam(query || "", "nonce"); if (!stateValue) { return false; } @@ -72,17 +70,18 @@ export class CsrfCheck { // 요청에서 보낸 state 추출 const query = request.getQuery() || ""; - const originalState = httpUtils.getQueryParam(query, "state"); + const originalState = + httpUtils.getQueryParam(query, "state") || + httpUtils.getQueryParam(query || "", "nonce"); // 리다이렉트 URL에서 쿼리 부분만 추출 const locationHeader = httpUtils.getHeaderValue( response.getHeaders(), "location" ); - const responseState = httpUtils.getQueryParamFromURI( - locationHeader || "", - "state" - ); + const responseState = + httpUtils.getQueryParamFromURI(locationHeader || "", "state") || + httpUtils.getQueryParamFromURI(locationHeader || "", "nonce"); // state가 없거나, 요청값과 다르면 CSRF 위험 if (!responseState) { diff --git a/packages/backend/src/controller/nonceCheck.ts b/packages/backend/src/controller/nonceCheck.ts index 383ca90..a27a4d6 100644 --- a/packages/backend/src/controller/nonceCheck.ts +++ b/packages/backend/src/controller/nonceCheck.ts @@ -1,4 +1,4 @@ -import type { Request } from "caido:utils"; +import type { Request, Response } from "caido:utils"; import { TokenLeakCheck } from "./tokenLeakCheck"; export class NonceCheckController{ @@ -6,8 +6,8 @@ export class NonceCheckController{ * 응답이 OIDC(OpenID Connect) 플로우인지 확인하는 메서드 */ - public static isOidcFlow(req: Request): boolean { - if(TokenLeakCheck.extractIdToken(req)) { + public static isOidcFlow(req: Request, res:Response): boolean { + if(TokenLeakCheck.extractIdToken(req, res)) { return true; } return false; @@ -15,10 +15,10 @@ export class NonceCheckController{ public static isNonceCheckRequest(req: Request): boolean { - const id_token = decodeIdToken(req); + const id_token = TokenLeakCheck.decodeIdToken(req); // 1. nonce 파라미터가 포함된 요청인지 확인 - if (id_token.includes("nonce=")) { + if (id_token && id_token.includes("nonce=")) { 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 ""; -} diff --git a/packages/backend/src/controller/tokenLeakCheck.ts b/packages/backend/src/controller/tokenLeakCheck.ts index 2248b49..5a05ce9 100644 --- a/packages/backend/src/controller/tokenLeakCheck.ts +++ b/packages/backend/src/controller/tokenLeakCheck.ts @@ -1,8 +1,8 @@ -import type { Request } from "caido:utils"; +import type { Request,Response } from "caido:utils"; import jwt from "jsonwebtoken"; export class TokenLeakCheck { - public static extractIdToken(req: Request): string | null { + public static extractIdToken(req: Request, res?: Response): string | null { // 1. Authorization 헤더 확인\\ const header = req.getHeaders() as Record; const authHeader = header["authorization"] || header["Authorization"]; @@ -16,19 +16,21 @@ export class TokenLeakCheck { return (query as Record).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]); + // 3. response 안에 id_token이 있을 경우 + if (res) { + const rawBody = res.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 | null { - const token = this.extractIdToken(req); + public static decodeIdToken(req: Request, res?: Response): Record | null { + const token = this.extractIdToken(req, res); if (!token) return null; const decoded = jwt.decode(token, { complete: true }); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3072b06..38a0602 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -6,6 +6,7 @@ import { CsrfCheck } from "./controller/csrfCheck"; import { PKCECheck } from "./controller/PKCECheck"; import { AccessTokenLeakController } from "./controller/accessTokenDetector"; import { ScopeDetection } from "./controller/scopeDetection"; +import { NonceCheckController } from "./controller/nonceCheck"; export type API = DefineAPI<{}>; @@ -15,10 +16,15 @@ const csrfCheck = new CsrfCheck(); const pkceCheckController = new PKCECheck(); const tokenCheck = new AccessTokenLeakController(); const ScopeDetectionController = new ScopeDetection(); +const nonceCheckController = new NonceCheckController(); export function init(sdk: SDK) { - // sdk.events.onInterceptRequest(async (sdk, req: Request) => { - // const result = csrfCheck.checker(req); + sdk.events.onInterceptResponse(async (sdk, req: Request, res: Response) => { + 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) { // await sdk.findings.create({ @@ -54,6 +60,33 @@ export function init(sdk: SDK) { // request: req, // 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: "", + }); + } + }); + */ + } +)}