From 858dfd16dc24600d4bb604ce9e6f457e66129be8 Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sun, 25 May 2025 21:43:21 +0900 Subject: [PATCH 1/3] =?UTF-8?q?FEAT=20:=20AccessToken=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=81=EC=A2=85=20=ED=86=A0=ED=81=B0=20=EC=A1=B4=EC=9E=AC=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20con?= =?UTF-8?q?troller=20=EC=9E=91=EC=84=B1,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/backend/src/controller/accessTokenDetector.ts diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts new file mode 100644 index 0000000..fb3d03f --- /dev/null +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -0,0 +1,146 @@ +import type { Request, Response } from "caido:utils"; + +// 토큰 누출 검사 결과를 담는 구조 +export interface TokenLeakResult { + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) +} + +// 액세스 토큰 누출 검사 클래스 +export class AccessTokenLeakController { + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testReq(request: Request): Promise { + + // === 1. URL에서 토큰 검사 === + const url = request.getUrl(); + + const extractedTokenFromUrl = this.extractTokenFromText(url); + + if (extractedTokenFromUrl) { + return { + found: true, + location: 'url', + title: "Access Token Leak in URL", + description: `요청 URL에 액세스 토큰 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, + value: url + }; + } + + // === 2. 요청 본문(Body)에서 토큰 검사 === + const body = request.getBody(); + + if (body) { + const bodyText = await body.toText(); + + const extractedTokenFromBody = this.extractTokenFromText(bodyText); + + if (extractedTokenFromBody) { + return { + found: true, + location: 'body', + title: "Access Token Leak in Request Body", + description: `요청 Body에 access_token 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + value: bodyText + }; + } + } + + return null; + } + + /** + * HTTP 응답에서 액세스 토큰 누출 검사 + * @param response - 검사할 HTTP 응답 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testResp(response: Response): Promise { + + // === 1. Location 헤더에서 토큰 검사 === + const locationHeader = response.getHeader("Location"); + + const locationHeaderStr = Array.isArray(locationHeader) ? locationHeader.join(', ') : locationHeader; + + if (locationHeaderStr) { + const extractedTokenFromHeader = this.extractTokenFromText(locationHeaderStr); + + if (extractedTokenFromHeader) { + return { + found: true, + location: 'header', + title: "Access Token Leak in Redirect URL", + description: `Location 헤더에 액세스 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, + value: locationHeaderStr + }; + } + } + + // === 2. 응답 본문에서 토큰 검사 === + const bodyBytes = response.getBody(); + + if (bodyBytes) { + const bodyText = await bodyBytes.toText(); + + const extractedTokenFromBody = this.extractTokenFromText(bodyText); + + if (extractedTokenFromBody) { + return { + found: true, + location: 'body', + title: "Access Token Leak in Response Body", + description: `HTTP 응답 본문에 'access_token' 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + value: bodyText + }; + } + } + + return null; + } + + /** + * 텍스트에서 실제 토큰 값을 추출 + * @param text - 검사할 텍스트 + * @returns 토큰 값이 있으면 해당 값, 없으면 null + */ +private extractTokenFromText(text: string): string | null { + // 토큰 관련 키워드 리스트 + const tokenKeys = [ + 'access_token', + 'id_token', + 'auth_token', + 'token', + 'jwt', + 'session_token' + ]; + + // 정규표현식 패턴 리스트 생성 + const tokenPatterns: RegExp[] = []; + + for (const key of tokenKeys) { + // 1. key=token 또는 key: token + tokenPatterns.push(new RegExp(`${key}[=:]\\s*([a-zA-Z0-9\\-._~+/]+=*)`, 'i')); + + // 2. JSON 형태의 "key": "token" + tokenPatterns.push(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`, 'i')); + } + + // 3. Authorization: Bearer 형태 + tokenPatterns.push(/bearer\s+([a-zA-Z0-9\-._~+/]+=*)/i); + + // 모든 패턴에 대해 검사 + for (const pattern of tokenPatterns) { + const match = pattern.exec(text); + if (match && match[1]) { + return match[1]; + } + } + + return null; + } +} \ No newline at end of file From 7b704cacf499a68cbc7a4d2cae058bca19d579af Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sat, 31 May 2025 11:55:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?STYLE=20:=20=EB=A1=9C=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts index fb3d03f..22be16e 100644 --- a/packages/backend/src/controller/accessTokenDetector.ts +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -2,21 +2,21 @@ import type { Request, Response } from "caido:utils"; // 토큰 누출 검사 결과를 담는 구조 export interface TokenLeakResult { - found: boolean; // 토큰이 발견되었는지 여부 (true/false) - location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) - title: string; // 경고 제목 - description: string; // 상세 설명 - value?: string; // 실제 발견된 값 (선택적) + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) } // 액세스 토큰 누출 검사 클래스 export class AccessTokenLeakController { - - /** - * @param request - 검사할 HTTP 요청 객체 - * @returns 토큰이 발견되면 결과 객체, 없으면 null - */ - async testReq(request: Request): Promise { + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testReq(request: Request): Promise { // === 1. URL에서 토큰 검사 === const url = request.getUrl(); @@ -28,7 +28,7 @@ export class AccessTokenLeakController { found: true, location: 'url', title: "Access Token Leak in URL", - description: `요청 URL에 액세스 토큰 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, + description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, value: url }; } @@ -46,7 +46,7 @@ export class AccessTokenLeakController { found: true, location: 'body', title: "Access Token Leak in Request Body", - description: `요청 Body에 access_token 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; } @@ -75,7 +75,7 @@ export class AccessTokenLeakController { found: true, location: 'header', title: "Access Token Leak in Redirect URL", - description: `Location 헤더에 액세스 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, + description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, value: locationHeaderStr }; } @@ -88,13 +88,13 @@ export class AccessTokenLeakController { const bodyText = await bodyBytes.toText(); const extractedTokenFromBody = this.extractTokenFromText(bodyText); - + if (extractedTokenFromBody) { return { found: true, location: 'body', title: "Access Token Leak in Response Body", - description: `HTTP 응답 본문에 'access_token' 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; } From f1b5ef5f9b668d57a2c9999b34e142531bc8afac Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sat, 31 May 2025 12:37:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?REFACTOR=20:=20findings=EB=A5=BCindex?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EB=AA=A8=EB=93=88=EC=95=A0?= =?UTF-8?q?=EC=84=9C=20=EB=A7=8C=EB=93=A4=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 48 ++++++++++++++----- packages/backend/src/index.ts | 4 ++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts index 22be16e..8093a54 100644 --- a/packages/backend/src/controller/accessTokenDetector.ts +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -1,22 +1,46 @@ import type { Request, Response } from "caido:utils"; +import type { SDK, DefineAPI } from "caido:plugin"; // 토큰 누출 검사 결과를 담는 구조 export interface TokenLeakResult { - found: boolean; // 토큰이 발견되었는지 여부 (true/false) - location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) - title: string; // 경고 제목 - description: string; // 상세 설명 - value?: string; // 실제 발견된 값 (선택적) + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) } // 액세스 토큰 누출 검사 클래스 export class AccessTokenLeakController { - - /** - * @param request - 검사할 HTTP 요청 객체 - * @returns 토큰이 발견되면 결과 객체, 없으면 null - */ - async testReq(request: Request): Promise { + async testReq(sdk: SDK>, request: Request): Promise { + const result = await this._scanRequest(request); + if (result) { + await sdk.findings.create({ + title: result.title, + description: result.description, + request, + reporter: "", + }); + } + } + + async testResp(sdk: SDK>, response: Response, request: Request): Promise { + const result = await this._scanResponse(response); + if (result) { + await sdk.findings.create({ + title: result.title, + description: result.description, + request, + reporter: "", + }); + } + } + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async _scanRequest(request: Request): Promise { // === 1. URL에서 토큰 검사 === const url = request.getUrl(); @@ -60,7 +84,7 @@ export class AccessTokenLeakController { * @param response - 검사할 HTTP 응답 객체 * @returns 토큰이 발견되면 결과 객체, 없으면 null */ - async testResp(response: Response): Promise { + async _scanResponse(response: Response): Promise { // === 1. Location 헤더에서 토큰 검사 === const locationHeader = response.getHeader("Location"); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a24d2c7..9cf32b2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -4,6 +4,7 @@ import type { Request, Response } from "caido:utils"; // import { AuthZCodeGrantController } from "./controller/authZCodeGrant"; import { CsrfCheck } from "./controller/csrfCheck"; import { PKCECheck } from "./controller/PKCECheck"; +import { AccessTokenLeakController } from "./controller/accessTokenDetector"; export type API = DefineAPI<{}>; @@ -11,6 +12,7 @@ const csrfCheck = new CsrfCheck(); // const implicitGrantController = new ImplicitGrantController(); // const authZCodeGrantController = new AuthZCodeGrantController(); const pkceCheckController = new PKCECheck(); +const tokenCheck = new AccessTokenLeakController(); export function init(sdk: SDK) { // sdk.events.onInterceptRequest(async (sdk, req: Request) => { @@ -30,6 +32,8 @@ export function init(sdk: SDK) { async (sdk: SDK, {}>, req: Request, resp: Response) => { await csrfCheck.checker(sdk, req, resp); await pkceCheckController.test(sdk, req); + await tokenCheck.testReq(sdk, req); + await tokenCheck.testResp(sdk, resp, req); // sdk.events.onInterceptRequest(async (sdk, req: Request) => { // const result = // authZCodeGrantController.testReq(req) ||