csrf(state) 관련 취약점 탐지 기능 추가

This commit is contained in:
tv0924@icloud.com 2025-05-28 14:11:53 +09:00
commit e868cbec67
5 changed files with 400 additions and 7 deletions

View file

@ -0,0 +1,178 @@
import type { Request, Response } from "caido:utils";
import type { SDK, DefineAPI } from "caido:plugin";
import { HttpUtils } from "../utils/http";
const httpUtils = new HttpUtils();
export class CsrfCheck {
private isOauthUri(request: Request): boolean {
const query = request.getQuery() || "";
// 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="))
) {
return true;
}
return false;
}
private isOauthRedirectResponse(response: Response): boolean {
const status = response.getCode();
const locationHeader = 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
) {
return true;
}
return false;
}
private isStateInQuery(request: Request): boolean {
const query = request.getQuery();
const stateValue = httpUtils.getQueryParam(query || "", "state");
if (!stateValue) {
return false;
}
return true;
}
private checkStateAtResponseLocationHeader(
request: Request,
response: Response
): string[] | 0 {
if (
!(
this.isOauthUri(request) &&
this.isStateInQuery(request) &&
this.isOauthRedirectResponse(response)
)
) {
return 0; // Not a target, no CSRF risk
}
// 요청에서 보낸 state 추출
const query = request.getQuery() || "";
const originalState = httpUtils.getQueryParam(query, "state");
// 리다이렉트 URL에서 쿼리 부분만 추출
const locationHeader = httpUtils.getHeaderValue(
response.getHeaders(),
"location"
);
const responseState = httpUtils.getQueryParamFromURI(
locationHeader || "",
"state"
);
// state가 없거나, 요청값과 다르면 CSRF 위험
if (!responseState) {
// missing state
return ["state parameter is missing in the response location header"];
}
if (originalState !== responseState) {
// mismatch
return ["state parameter mismatch between request and response"];
}
return 0; // no CSRF risk detected
}
// private async checkStateReuse(
// request: Request,
// originResponse: Response
// ): Promise<string[] | 0> {
// // uri에 oauth 관련 파라미터가 없지만, 응답이 oauth 리다이렉트 응답인지 확인
// // 즉, 처음으로 state를 발급한 요청인지 확인
// if (
// !(
// !this.isOauthUri(request) &&
// this.isOauthRedirectResponse(originResponse)
// )
// ) {
// return 0; // Not a target, no CSRF risk
// }
// const originResponseLocationHeader = httpUtils.getHeaderValue(
// originResponse.getHeaders(),
// "location"
// );
// const originState = httpUtils.getQueryParamFromURI(
// originResponseLocationHeader || "",
// "state"
// );
// const requestHeaders = request.getHeaders();
// const noCookieHeaders = httpUtils.removeHeaders(requestHeaders, ["cookie"]);
// const newResponse = await httpUtils.resend(request, {
// headers: noCookieHeaders,
// });
// const newLocationHeader = httpUtils.getHeaderValue(
// newResponse.getHeaders(),
// "location"
// );
// const newState = httpUtils.getQueryParamFromURI(
// newLocationHeader || "",
// "state"
// );
// if (originState === newState) {
// return [
// "State parameter reused in the response location header, indicating a potential CSRF risk",
// ];
// }
// return 0; // no CSRF risk detected
// }
async checker(
sdk: SDK<DefineAPI<{}>, {}>,
request: Request,
response: Response
): Promise<string | 0> {
let result = ``;
// 쿼리에 state 파라미터가 없으면 CSRF 위험
if (this.isOauthUri(request) && !this.isStateInQuery(request)) {
result += "CSRF risk: missing state parameter"; // CSRF risk: missing state parameter
}
// location 헤더에 state 파라미터가 없거나, 요청에서 보낸 state와 다르면 CSRF 위험
const stateAtResponseLocationHeaderCheck =
this.checkStateAtResponseLocationHeader(request, response);
if (stateAtResponseLocationHeaderCheck !== 0) {
result += `, ${stateAtResponseLocationHeaderCheck.join(", ")}`;
}
// // 처음으로 state를 발급한 요청에서 state 파라미터를 바꿔서 보내기
// const reusedStateCheck = await this.checkStateReuse(request, response);
// if (reusedStateCheck !== 0) {
// result += `, ${reusedStateCheck.join(", ")}`;
// }
if (result) {
return result; // CSRF risk detected
} else {
return 0; // No CSRF risk detected
}
}
}