csrf(state) 관련 취약점 탐지 기능 추가
This commit is contained in:
parent
11b6e479dd
commit
e868cbec67
5 changed files with 400 additions and 7 deletions
178
packages/backend/src/controller/csrfCheck.ts
Normal file
178
packages/backend/src/controller/csrfCheck.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue