Merge branch 'main' into feature/scope
This commit is contained in:
commit
307d373b9c
15 changed files with 1841 additions and 38 deletions
BIN
dist/plugin_package.zip
vendored
Normal file
BIN
dist/plugin_package.zip
vendored
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "caido-oauth",
|
||||
"name": "caido-oauth-dev",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
|
|||
181
packages/backend/src/controller/csrfCheck.ts
Normal file
181
packages/backend/src/controller/csrfCheck.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
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<void> {
|
||||
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) {
|
||||
await sdk.findings.create({
|
||||
title: "csrf vuln",
|
||||
description: `SSO-related parameters detected in response:\n\n${request.getMethod()} ${request.getUrl()} : ${result}`,
|
||||
request,
|
||||
reporter: "csrf reporter",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +1,53 @@
|
|||
import type { SDK, DefineAPI } from "caido:plugin";
|
||||
import type { Request } from "caido:utils";
|
||||
import { ImplicitGrantController } from "./controller/implictGrant";
|
||||
import { AuthZCodeGrantController } from "./controller/authZCodeGrant";
|
||||
import type { Request, Response } from "caido:utils";
|
||||
// import { ImplicitGrantController } from "./controller/implictGrant";
|
||||
// import { AuthZCodeGrantController } from "./controller/authZCodeGrant";
|
||||
import { CsrfCheck } from "./controller/csrfCheck";
|
||||
import { PKCECheck } from "./controller/PKCECheck";
|
||||
import { ScopeDetection } from "./controller/scopeDetection";
|
||||
|
||||
export type API = DefineAPI<{}>;
|
||||
|
||||
const implicitGrantController = new ImplicitGrantController();
|
||||
const authZCodeGrantController = new AuthZCodeGrantController();
|
||||
const csrfCheck = new CsrfCheck();
|
||||
// const implicitGrantController = new ImplicitGrantController();
|
||||
// const authZCodeGrantController = new AuthZCodeGrantController();
|
||||
const pkceCheckController = new PKCECheck();
|
||||
const ScopeDetectionController = new ScopeDetection();
|
||||
|
||||
// function matchSSORequest(req: Request): boolean {
|
||||
// const raw = req.getRaw().toString();
|
||||
|
||||
// // 조건 3: Raw request에 SAMLRequest 또는 SAMLResponse 포함
|
||||
// if (raw.includes("SAMLRequest=") || raw.includes("SAMLResponse=")) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// function matchAccessTokenResponse(resp: Response): boolean {
|
||||
// const raw = resp.getRaw().toString();
|
||||
|
||||
// const match = /"access_token"\s*:\s*"([^"]+)"/.exec(raw);
|
||||
// return !!match;
|
||||
// }
|
||||
|
||||
export function init(sdk: SDK<API>) {
|
||||
sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
const result =
|
||||
authZCodeGrantController.testReq(req) ||
|
||||
implicitGrantController.testReq(req);
|
||||
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
// const result = csrfCheck.checker(req);
|
||||
|
||||
if (result) {
|
||||
// if (result) {
|
||||
// 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: "",
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
sdk.events.onInterceptResponse(
|
||||
async (sdk: SDK<DefineAPI<{}>, {}>, req: Request, resp: Response) => {
|
||||
await csrfCheck.checker(sdk, req, resp);
|
||||
await pkceCheckController.test(sdk, req);
|
||||
await ScopeDetectionController.scan(sdk, req.getUrl());
|
||||
|
||||
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: "",
|
||||
});
|
||||
}
|
||||
// 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: "",
|
||||
// });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
210
packages/backend/src/utils/http.ts
Normal file
210
packages/backend/src/utils/http.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
let instance: HttpUtils | null = null;
|
||||
export class HttpUtils {
|
||||
/**
|
||||
* 싱글턴 인스턴스를 생성합니다.
|
||||
*/
|
||||
public constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
instance = this;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* URI 디코딩 후 소문자로 변환하는 헬퍼 함수
|
||||
* @param value - 디코딩하고 소문자로 변환할 문자열
|
||||
* @returns 디코딩 및 소문자 변환된 문자열
|
||||
*/
|
||||
decodeAndLower(value: string): string {
|
||||
try {
|
||||
return decodeURIComponent(value).toLowerCase();
|
||||
} catch {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 객체의 키와 값을 전부 소문자로 변환합니다.
|
||||
* @param headers - Record<string, string | string[]> 형태의 헤더 맵
|
||||
* @returns - 키와 값이 모두 소문자로 변환된 새 헤더 맵
|
||||
*/
|
||||
lowerCaseAllHeaders(
|
||||
headers: Record<string, string | string[]>
|
||||
): Record<string, string | string[]> {
|
||||
const result: Record<string, string | string[]> = {};
|
||||
|
||||
for (const [rawKey, rawValue] of Object.entries(headers)) {
|
||||
const key = this.decodeAndLower(rawKey);
|
||||
|
||||
if (Array.isArray(rawValue)) {
|
||||
result[key] = rawValue.map((v) => this.decodeAndLower(v));
|
||||
} else {
|
||||
result[key] = this.decodeAndLower(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getQueryParamFromURI(uri: string, key: string): string | null {
|
||||
uri = uri.toLowerCase();
|
||||
key = key.toLowerCase();
|
||||
try {
|
||||
const urlObj = new URL(uri);
|
||||
return urlObj.searchParams.get(key);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Query
|
||||
/**
|
||||
* 주어진 쿼리 문자열(query)에서 key에 해당하는 값을 반환합니다.
|
||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
||||
* @param key - 가져오고 싶은 파라미터 이름
|
||||
* @returns - 해당 파라미터 값, 없으면 null
|
||||
*/
|
||||
getQueryParam(query: string, key: string): string | null {
|
||||
query = query.toLowerCase();
|
||||
key = key.toLowerCase();
|
||||
|
||||
const params = new URLSearchParams(query);
|
||||
return params.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 쿼리 문자열(query)에 key=value를 설정하고, 전체 쿼리 문자열을 반환합니다.
|
||||
* - 이미 key가 있으면 덮어쓰기(set), 없으면 새로 추가합니다.
|
||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
||||
* @param key - 설정할 파라미터 이름
|
||||
* @param value - 설정할 값
|
||||
* @returns - "a=1&b=2&c=3..." 형태의 새로운 쿼리 문자열
|
||||
*/
|
||||
setQueryParam(query: string, key: string, value: string): string {
|
||||
query = query.toLowerCase();
|
||||
key = key.toLowerCase();
|
||||
value = value.toLowerCase();
|
||||
|
||||
const params = new URLSearchParams(query);
|
||||
params.set(key, value);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 쿼리 문자열(query)에서 key에 해당하는 파라미터를 삭제(delete)하고,
|
||||
* 전체 쿼리 문자열을 반환합니다.
|
||||
* @param query - "a=1&b=2..." 형태의 쿼리 문자열 (맨 앞 ? 는 없어야 합니다)
|
||||
* @param key - 삭제할 파라미터 이름
|
||||
* @returns - 삭제된 상태의 새로운 쿼리 문자열
|
||||
*/
|
||||
removeQueryParam(query: string, key: string): string {
|
||||
query = query.toLowerCase();
|
||||
key = key.toLowerCase();
|
||||
|
||||
const params = new URLSearchParams(query);
|
||||
params.delete(key);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Headers
|
||||
/**
|
||||
* 주어진 헤더 맵에서 name에 해당하는 첫 번째 헤더 값을 반환합니다.
|
||||
* @param headers - Response.getHeaders() 가 반환하는 객체
|
||||
* @param name - 꺼내고 싶은 헤더 이름 (예: "location", "Content-Type")
|
||||
* @returns - 해당 헤더의 첫 번째 값, 없으면 null
|
||||
*/
|
||||
getHeaderValue(
|
||||
headers: Record<string, string | string[]>,
|
||||
name: string
|
||||
): string | null {
|
||||
const normalized = this.lowerCaseAllHeaders(headers);
|
||||
const target = name.toLowerCase();
|
||||
|
||||
for (const [key, value] of Object.entries(normalized)) {
|
||||
if (key === target) {
|
||||
let rawValue: string | null = null;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
rawValue = value.length > 0 && value[0] ? value[0] : null;
|
||||
} else {
|
||||
rawValue = value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
if (rawValue !== null) {
|
||||
try {
|
||||
return decodeURIComponent(rawValue);
|
||||
} catch {
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 헤더 맵에서 name에 해당하는 헤더 값을 value로 변경한 새 맵을 반환합니다.
|
||||
* - 기존 헤더 이름의 대소문자를 보존합니다.
|
||||
* - value가 string인 경우 [value] 형태로, string[]인 경우 그대로 사용합니다.
|
||||
* - 기존에 해당 헤더가 없으면 새로 추가합니다.
|
||||
*
|
||||
* @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵
|
||||
* @param name - 변경할 헤더 이름 (예: "Authorization", "X-Custom-Header")
|
||||
* @param value - 새로 설정할 값 (string 또는 string[])
|
||||
* @returns - 지정된 헤더가 업데이트된 새로운 헤더 맵
|
||||
*/
|
||||
setHeaderValue(
|
||||
headers: Record<string, string | string[]>,
|
||||
name: string,
|
||||
value: string | string[]
|
||||
): Record<string, string[]> {
|
||||
headers = this.lowerCaseAllHeaders(headers);
|
||||
const lowerName = name.toLowerCase();
|
||||
const newHeaders: Record<string, string[]> = {};
|
||||
|
||||
// 1) 기존 헤더 복사하되, name과 일치하는 항목은 value로 대체
|
||||
for (const [key, vals] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === lowerName) {
|
||||
newHeaders[key] = Array.isArray(value) ? value : [value];
|
||||
} else {
|
||||
newHeaders[key] = Array.isArray(vals) ? vals : [vals];
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 해당 헤더가 원래 없었다면 새로 추가
|
||||
const exists = Object.keys(newHeaders).some(
|
||||
(k) => k.toLowerCase() === lowerName
|
||||
);
|
||||
if (!exists) {
|
||||
newHeaders[name] = Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
return newHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 헤더 맵에서 특정 이름(들)에 해당하는 헤더를 제거한 새 맵을 반환합니다.
|
||||
* @param headers - 키가 헤더 이름, 값이 문자열 배열인 헤더 맵
|
||||
* @param namesToRemove - 제거할 헤더 이름(하나 혹은 배열). 대소문자 구분 없이 매칭됩니다.
|
||||
* @returns - 지정된 헤더가 제외된 새로운 헤더 맵
|
||||
*/
|
||||
removeHeaders(
|
||||
headers: Record<string, string | string[]>,
|
||||
namesToRemove: string | string[]
|
||||
): Record<string, string[]> {
|
||||
headers = this.lowerCaseAllHeaders(headers);
|
||||
const toRemove = Array.isArray(namesToRemove)
|
||||
? namesToRemove.map((n) => n.toLowerCase())
|
||||
: [namesToRemove.toLowerCase()];
|
||||
|
||||
const filtered: Record<string, string[]> = {};
|
||||
for (const [key, vals] of Object.entries(headers)) {
|
||||
if (!toRemove.includes(key.toLowerCase())) {
|
||||
filtered[key] = Array.isArray(vals) ? vals : [vals];
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
65
playground/csrf/index.js
Normal file
65
playground/csrf/index.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// app.js
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const port = 8000;
|
||||
|
||||
// 콜백 엔드포인트 (정상 동작 시뮬레이션)
|
||||
app.get("/callback", (req, res) => {
|
||||
res.send(`
|
||||
<h1>Callback Received</h1>
|
||||
<p>Query Params:</p>
|
||||
<pre>${JSON.stringify(req.query, null, 2)}</pre>
|
||||
`);
|
||||
});
|
||||
|
||||
/**
|
||||
* 1) state 파라미터를 무시하는 취약한 /authorize 엔드포인트
|
||||
* - 클라이언트가 state를 보내도 무시
|
||||
* - 리디렉트 시 state를 포함하지 않음
|
||||
*/
|
||||
app.get("/authorize/no-state", (req, res) => {
|
||||
const clientId = req.query.client_id || "unknown-client";
|
||||
const redirectUri = encodeURIComponent(
|
||||
req.query.redirect_uri || `http://localhost:${port}/callback`
|
||||
);
|
||||
const code = "authcode-12345";
|
||||
|
||||
// state를 전혀 포함하지 않은 채로 리디렉트
|
||||
const location = `${redirectUri}?code=${code}&client_id=${clientId}`;
|
||||
res.set("Location", location);
|
||||
res.status(302).send(`Redirecting to ${location}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* 2) 클라이언트가 보낸 state와 다른 값을 넣는 취약한 /authorize 엔드포인트
|
||||
* - 클라이언트가 보낸 state를 로그로 확인만 하고,
|
||||
* 응답 Location에는 'wrong-state'를 삽입
|
||||
*/
|
||||
app.get("/authorize/mismatch-state", (req, res) => {
|
||||
const clientId = req.query.client_id || "unknown-client";
|
||||
const originalState = req.query.state;
|
||||
const redirectUri = encodeURIComponent(
|
||||
req.query.redirect_uri || `http://localhost:${port}/callback`
|
||||
);
|
||||
const code = "authcode-67890";
|
||||
|
||||
console.log(`[VULN] original state from client:`, originalState);
|
||||
|
||||
// 클라이언트 state와 다르게 'wrong-state'를 삽입
|
||||
const wrongState = "wrong-state";
|
||||
const location = `${redirectUri}?code=${code}&state=${wrongState}&client_id=${clientId}`;
|
||||
res.set("Location", location);
|
||||
res.status(302).send(`Redirecting to ${location}`);
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(
|
||||
`Vulnerable OAuth test server listening at http://localhost:${port}`
|
||||
);
|
||||
console.log(
|
||||
`1) No-State: http://localhost:${port}/authorize/no-state?client_id=abc&redirect_uri=http://localhost:${port}/callback`
|
||||
);
|
||||
console.log(
|
||||
`2) Mismatch-State: http://localhost:${port}/authorize/mismatch-state?client_id=abc&state=xyz&redirect_uri=http://localhost:${port}/callback`
|
||||
);
|
||||
});
|
||||
1167
playground/csrf/package-lock.json
generated
Normal file
1167
playground/csrf/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
playground/csrf/package.json
Normal file
19
playground/csrf/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "csrf",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"keywords": [],
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
34
playground/pkce/.gitignore
vendored
Normal file
34
playground/pkce/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
playground/pkce/README.md
Normal file
15
playground/pkce/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# playground
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.14. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
25
playground/pkce/bun.lock
Normal file
25
playground/pkce/bun.lock
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "playground",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
}
|
||||
}
|
||||
10
playground/pkce/package.json
Normal file
10
playground/pkce/package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "playground",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
31
playground/pkce/src/PKCEDowngradeExpress.js
Normal file
31
playground/pkce/src/PKCEDowngradeExpress.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const express = require("express");
|
||||
const app = express();
|
||||
|
||||
app.get("/auth", (req, res) => {
|
||||
const {
|
||||
client_id,
|
||||
response_type,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
scope
|
||||
} = req.query;
|
||||
|
||||
console.log("Incoming request:", req.query);
|
||||
|
||||
if (!client_id || response_type !== "code") {
|
||||
return res.status(400).send("Missing required parameters");
|
||||
}
|
||||
|
||||
// Simulate issuing an authorization code
|
||||
const code = "dummy-auth-code";
|
||||
|
||||
// Simulate PKCE check (normally you'd validate here)
|
||||
// We deliberately allow the downgrade here to simulate the vulnerability
|
||||
const responseBody = `Authorization successful. code=${code}`;
|
||||
return res.status(200).send(responseBody);
|
||||
});
|
||||
|
||||
const PORT = 5050;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Test PKCE server running on http://localhost:${PORT}`);
|
||||
});
|
||||
29
playground/pkce/tsconfig.json
Normal file
29
playground/pkce/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,9 @@ importers:
|
|||
'@caido-community/dev':
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.5(postcss@8.5.3)(typescript@5.5.4)
|
||||
'@caido/sdk-backend':
|
||||
specifier: ^0.48.1
|
||||
version: 0.48.1
|
||||
typescript:
|
||||
specifier: 5.5.4
|
||||
version: 5.5.4
|
||||
|
|
@ -34,9 +37,15 @@ packages:
|
|||
'@caido/quickjs-types@0.17.2':
|
||||
resolution: {integrity: sha512-5kcucGORMNEbcdU91yKLYZG/TFDqsO6XmCZ1TnU6V48E61mmqrJg6kjrfOFP1WOugDm+ZcGd/Su3p3XkFXfaPg==}
|
||||
|
||||
'@caido/quickjs-types@0.18.0':
|
||||
resolution: {integrity: sha512-hRXUVdDvlhEhvkBoWWytoVS2j1KDVZa8dx2Q/KvWUQTR57U8EMSYE9iFgvPhu78gS8z+RF42Zcb7moNx4SDMlw==}
|
||||
|
||||
'@caido/sdk-backend@0.46.0':
|
||||
resolution: {integrity: sha512-peUKW/4Nrw9WVxIahc+6KrVtxA7vsbpuJqOoBxudxq7tQJ+cV9IEqzvYoFFo8KlnrTkeUQUJvd0W4WsM3HgxEg==}
|
||||
|
||||
'@caido/sdk-backend@0.48.1':
|
||||
resolution: {integrity: sha512-JvFeOlSqAKbj3OenBn0LPtCNaOV0x6YtaAQijpvYfBJK32Nvbf924Z10bFVCu+Clc5A1qr7HcAvJ/8B/aRikWA==}
|
||||
|
||||
'@caido/sdk-shared@0.1.1':
|
||||
resolution: {integrity: sha512-JAV5ajUqxZdXYPTmDEvIKBZon8I5uHq44ATj0Nj3BVpllRDUGY9kcBd+PXMD50+3lv1CvhR3/f6q24T0+4aVJQ==}
|
||||
|
||||
|
|
@ -1095,11 +1104,18 @@ snapshots:
|
|||
|
||||
'@caido/quickjs-types@0.17.2': {}
|
||||
|
||||
'@caido/quickjs-types@0.18.0': {}
|
||||
|
||||
'@caido/sdk-backend@0.46.0':
|
||||
dependencies:
|
||||
'@caido/quickjs-types': 0.17.2
|
||||
'@caido/sdk-shared': 0.1.1
|
||||
|
||||
'@caido/sdk-backend@0.48.1':
|
||||
dependencies:
|
||||
'@caido/quickjs-types': 0.18.0
|
||||
'@caido/sdk-shared': 0.1.1
|
||||
|
||||
'@caido/sdk-shared@0.1.1': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue