Merge branch 'main' of https://github.com/whs-authz-authn-project/caido-plugin-test into feature/pkce
This commit is contained in:
commit
d820695cec
5 changed files with 100 additions and 62 deletions
|
|
@ -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'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 rawBody = res.getRaw();
|
||||||
const body = rawBody ? rawBody.toString() : "";
|
const body = rawBody ? rawBody.toString() : "";
|
||||||
const match = body.match(/id_token=([^&\s]+)/);
|
const match = body.match(/id_token=([^&\s]+)/);
|
||||||
if (match && typeof match[1] === "string") {
|
if (match && typeof match[1] === "string" ) {
|
||||||
return decodeURIComponent(match[1]);
|
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 });
|
||||||
|
|
|
||||||
|
|
@ -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: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue