Merge branch 'main' into feature/access-token-detector
This commit is contained in:
commit
b1c10b0739
12 changed files with 81 additions and 226 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -220,5 +220,6 @@ dist/*
|
|||
packages/frontend/dist
|
||||
packages/backend/dist
|
||||
#!dist/*.zip
|
||||
dist/plugin_package.zip
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,linux
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,linux
|
||||
|
|
|
|||
BIN
dist/plugin_package.zip
vendored
BIN
dist/plugin_package.zip
vendored
Binary file not shown.
|
|
@ -5,18 +5,27 @@ import { HttpUtils } from "../utils/http";
|
|||
const httpUtils = new HttpUtils();
|
||||
|
||||
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 {
|
||||
const query = request.getQuery() || "";
|
||||
const uri = request.getUrl() || "";
|
||||
|
||||
// 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="))
|
||||
) {
|
||||
if (this.isTargetUri(uri)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -25,23 +34,10 @@ export class CsrfCheck {
|
|||
|
||||
private isOauthRedirectResponse(response: Response): boolean {
|
||||
const status = response.getCode();
|
||||
const locationHeader = httpUtils.getHeaderValue(
|
||||
response.getHeaders(),
|
||||
"location"
|
||||
);
|
||||
const uri =
|
||||
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
|
||||
) {
|
||||
if (status >= 300 && status < 400 && this.isTargetUri(uri)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -49,7 +45,9 @@ export class CsrfCheck {
|
|||
|
||||
private isStateInQuery(request: Request): boolean {
|
||||
const query = request.getQuery();
|
||||
const stateValue = httpUtils.getQueryParam(query || "", "state");
|
||||
const stateValue =
|
||||
httpUtils.getQueryParam(query || "", "state") ||
|
||||
httpUtils.getQueryParam(query || "", "nonce");
|
||||
if (!stateValue) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -72,17 +70,18 @@ export class CsrfCheck {
|
|||
|
||||
// 요청에서 보낸 state 추출
|
||||
const query = request.getQuery() || "";
|
||||
const originalState = httpUtils.getQueryParam(query, "state");
|
||||
const originalState =
|
||||
httpUtils.getQueryParam(query, "state") ||
|
||||
httpUtils.getQueryParam(query || "", "nonce");
|
||||
|
||||
// 리다이렉트 URL에서 쿼리 부분만 추출
|
||||
const locationHeader = httpUtils.getHeaderValue(
|
||||
response.getHeaders(),
|
||||
"location"
|
||||
);
|
||||
const responseState = httpUtils.getQueryParamFromURI(
|
||||
locationHeader || "",
|
||||
"state"
|
||||
);
|
||||
const responseState =
|
||||
httpUtils.getQueryParamFromURI(locationHeader || "", "state") ||
|
||||
httpUtils.getQueryParamFromURI(locationHeader || "", "nonce");
|
||||
|
||||
// state가 없거나, 요청값과 다르면 CSRF 위험
|
||||
if (!responseState) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Request } from "caido:utils";
|
||||
import type { Request, Response } from "caido:utils";
|
||||
import { TokenLeakCheck } from "./tokenLeakCheck";
|
||||
|
||||
export class NonceCheckController{
|
||||
|
|
@ -6,8 +6,8 @@ export class NonceCheckController{
|
|||
* 응답이 OIDC(OpenID Connect) 플로우인지 확인하는 메서드
|
||||
*/
|
||||
|
||||
public static isOidcFlow(req: Request): boolean {
|
||||
if(TokenLeakCheck.extractIdToken(req)) {
|
||||
public static isOidcFlow(req: Request, res:Response): boolean {
|
||||
if(TokenLeakCheck.extractIdToken(req, res)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -15,10 +15,10 @@ export class NonceCheckController{
|
|||
|
||||
|
||||
public static isNonceCheckRequest(req: Request): boolean {
|
||||
const id_token = decodeIdToken(req);
|
||||
const id_token = TokenLeakCheck.decodeIdToken(req);
|
||||
|
||||
// 1. nonce 파라미터가 포함된 요청인지 확인
|
||||
if (id_token.includes("nonce=")) {
|
||||
if (id_token && id_token.includes("nonce=")) {
|
||||
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";
|
||||
|
||||
export class TokenLeakCheck {
|
||||
public static extractIdToken(req: Request): string | null {
|
||||
public static extractIdToken(req: Request, res?: Response): string | null {
|
||||
// 1. Authorization 헤더 확인\\
|
||||
const header = req.getHeaders() as Record<string, string | string[] | undefined>;
|
||||
const authHeader = header["authorization"] || header["Authorization"];
|
||||
|
|
@ -16,19 +16,21 @@ export class TokenLeakCheck {
|
|||
return (query as Record<string, any>).id_token;
|
||||
}
|
||||
|
||||
// 3. POST 바디 안에 id_token이 있을 경우
|
||||
const rawBody = req.getRaw();
|
||||
const body = rawBody ? rawBody.toString() : "";
|
||||
const match = body.match(/id_token=([^&\s]+)/);
|
||||
if (match && typeof match[1] === "string") {
|
||||
return decodeURIComponent(match[1]);
|
||||
// 3. response 안에 id_token이 있을 경우
|
||||
if (res) {
|
||||
const rawBody = res.getRaw();
|
||||
const body = rawBody ? rawBody.toString() : "";
|
||||
const match = body.match(/id_token=([^&\s]+)/);
|
||||
if (match && typeof match[1] === "string" ) {
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static decodeIdToken(req: Request): Record<string, any> | null {
|
||||
const token = this.extractIdToken(req);
|
||||
public static decodeIdToken(req: Request, res?: Response): Record<string, any> | null {
|
||||
const token = this.extractIdToken(req, res);
|
||||
if (!token) return null;
|
||||
|
||||
const decoded = jwt.decode(token, { complete: true });
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { CsrfCheck } from "./controller/csrfCheck";
|
|||
import { PKCECheck } from "./controller/PKCECheck";
|
||||
import { AccessTokenLeakController } from "./controller/accessTokenDetector";
|
||||
import { ScopeDetection } from "./controller/scopeDetection";
|
||||
import { NonceCheckController } from "./controller/nonceCheck";
|
||||
|
||||
export type API = DefineAPI<{}>;
|
||||
|
||||
|
|
@ -15,42 +16,42 @@ const csrfCheck = new CsrfCheck();
|
|||
const pkceCheckController = new PKCECheck();
|
||||
const tokenCheck = new AccessTokenLeakController();
|
||||
const ScopeDetectionController = new ScopeDetection();
|
||||
const nonceCheckController = new NonceCheckController();
|
||||
|
||||
export function init(sdk: SDK<API>) {
|
||||
// sdk.events.onInterceptRequest(async (sdk, req: Request) => {
|
||||
// const result = csrfCheck.checker(req);
|
||||
sdk.events.onInterceptResponse(async (sdk, req: Request, res: Response) => {
|
||||
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) {
|
||||
// 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 tokenCheck.testReq(sdk, req);
|
||||
await tokenCheck.testResp(sdk, resp, req);
|
||||
await ScopeDetectionController.scan(sdk, req.getUrl());
|
||||
// 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: "",
|
||||
// });
|
||||
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: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
34
playground/pkce/.gitignore
vendored
34
playground/pkce/.gitignore
vendored
|
|
@ -1,34 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "playground",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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}`);
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue