From dfeab629d771a8b3f7802b5f8dca7696606fe661 Mon Sep 17 00:00:00 2001 From: seungyeoncherry Date: Sat, 31 May 2025 11:49:11 +0900 Subject: [PATCH 1/7] [Add] Scope Detection --- .../backend/src/controller/scopeDetection.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/backend/src/controller/scopeDetection.ts diff --git a/packages/backend/src/controller/scopeDetection.ts b/packages/backend/src/controller/scopeDetection.ts new file mode 100644 index 0000000..9c8610e --- /dev/null +++ b/packages/backend/src/controller/scopeDetection.ts @@ -0,0 +1,89 @@ +import type { DefineAPI, SDK } from "caido:plugin"; +import { RequestSpec } from "caido:utils"; + +const scan = async ( // scan 함수 정의 + sdk: SDK, + url: string +): Promise<{ data: string }> => { + sdk.console.log(`들어온 url : ${url}`); // url이 잘 들어왔는지 확인함요 + + // url이 string이 아니고 , 값이 없거나 그럴 때 유효한 값 넣으라고 출력. + if (!url || typeof url !== "string") { + sdk.console.log("이상한 url 입력함."); + return { data: "알맞은 URL을 입력하세요." }; + } + + try { + const spec = new RequestSpec(url); // url에 GET 요청 보낼거긔. + spec.setMethod("GET"); + spec.setHeader("User-Agent", "Caido Scanner"); + spec.setHeader("Accept", "*/*"); + sdk.console.log(`요청 URL: ${url}`); + + const res = await sdk.requests.send(spec); // 요청 보내고 응답 받음. + sdk.console.log('[SCAN] 응답 :', res); + sdk.console.log(`[SCAN] 요청 성공:${(res as any).status}`); + sdk.console.log(`[SCAN] body: ${(res as any).body ? (res as any).body.toString().substring(0, 100) : "없음"}`); + + const html = (res as any).body ? (res as any).body.toString() : ""; + + // ]*href="([^"]+)"[^>]*>/gi; + const anchors: string[] = []; + let match; + while ((match = anchorRegex.exec(html)) !== null) { // html에서 a href 찾아 배열에 저장함. + if (typeof match[1] === "string") { + anchors.push(match[1]); + } + } + sdk.console.log(`찾아진 a href 개수: ${anchors.length}`); + + // 5. scope 탐지 + const results: string[] = []; + anchors.forEach((href) => { // 추출한 a href 링크 하나씩 검사드감. + try { + const absHref = new URL(href, url).href; // 상대경로라면 url 기준으로 절대 URL 바꿔줌줌 + sdk.console.log(`[SCAN] 절대 URL 변환: ${href} -> ${absHref}`); // + + if (/oauth|authorize|login|accounts|auth/i.test(absHref)) { // url에 이런 OAuth 키워드가 있는지 필터링. + let u: URL; + try { + u = new URL(absHref); // 필터링된 url을 url 객체로 파싱. 정식 url인 경우 변수 u에 저장. + } catch (err) { // 파싱 실패하면 + sdk.console.log( + `URL 파싱 실패 : ${absHref} (${err instanceof Error ? err.message : err})` + ); + return; + } + + try { + const scope = u.searchParams.get("scope"); // url에 scope있긔?scope값 가져와. + if (scope && /all|\*/i.test(scope)) { // scope가 존재하고 all, *있다면. + results.push(`위험한 scope 발견: ${scope}\n -> ${absHref}`); // results에 경고 메시지 전달. + } + } catch (err) { + sdk.console.log(`searchParams.get 실패`); + } + } + } catch (e) { + sdk.console.log( + `URL 파싱 실패 (absHref 단계): ${href} (${e instanceof Error ? e.message : e})` + ); + } + }); + + const resultStr = results.join("\n") || "위험한 scope가 발견되지 않았습니다."; + return { data: resultStr }; // 성공했는지 실패했는지 App.vue한테 전달할 메시지. + } catch (e) { + sdk.console.log(`백엔드 에러: ${e instanceof Error ? e.message : e}`); + return { data: "백엔드 에러: " + (e instanceof Error ? e.message : String(e)) }; // App.vue에 전달할 메시지. + } +}; + +export type API = DefineAPI<{ + scan: typeof scan; +}>; + +export function init(sdk: SDK) { + sdk.api.register("scan", scan); +} \ No newline at end of file From b1f3534e1cd6d3f0f33d6a3b563d965784929454 Mon Sep 17 00:00:00 2001 From: imnyang Date: Sat, 31 May 2025 11:55:15 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=ED=8F=AC=ED=8C=85=EC=9D=80=20=ED=96=88?= =?UTF-8?q?=EB=8A=94=EB=8D=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=8A=94=20?= =?UTF-8?q?=EC=95=88=ED=95=B4=EB=B3=B4=EA=B8=B4=20=ED=96=88=EC=96=B4?= =?UTF-8?q?=EC=9A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=A2=80=20=ED=95=B4?= =?UTF-8?q?=EC=A3=BC=EC=84=B8=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 52 ++++-- package.json | 4 + .../backend/src/controller/scopeDetection.ts | 148 +++++++++--------- packages/backend/src/index.ts | 3 + 4 files changed, 119 insertions(+), 88 deletions(-) diff --git a/bun.lock b/bun.lock index 289e8aa..6fa7969 100644 --- a/bun.lock +++ b/bun.lock @@ -3,6 +3,10 @@ "workspaces": { "": { "name": "caido-oauth", + "dependencies": { + "@types/jsonwebtoken": "^9.0.9", + "jsonwebtoken": "^9.0.2", + }, "devDependencies": { "@caido-community/dev": "^0.1.3", "@caido/sdk-backend": "^0.48.1", @@ -127,6 +131,12 @@ "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -143,6 +153,8 @@ "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -185,6 +197,8 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -261,8 +275,14 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -271,6 +291,20 @@ "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], @@ -291,7 +325,7 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -357,6 +391,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], @@ -419,6 +455,8 @@ "typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -449,14 +487,14 @@ "body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "finalhandler/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "router/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -477,12 +515,6 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "router/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -491,8 +523,6 @@ "sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "tsup/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index 7d2ba1b..a1e0324 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,9 @@ "@caido-community/dev": "^0.1.3", "@caido/sdk-backend": "^0.48.1", "typescript": "5.5.4" + }, + "dependencies": { + "@types/jsonwebtoken": "^9.0.9", + "jsonwebtoken": "^9.0.2" } } diff --git a/packages/backend/src/controller/scopeDetection.ts b/packages/backend/src/controller/scopeDetection.ts index 9c8610e..9b74dcc 100644 --- a/packages/backend/src/controller/scopeDetection.ts +++ b/packages/backend/src/controller/scopeDetection.ts @@ -1,89 +1,83 @@ -import type { DefineAPI, SDK } from "caido:plugin"; +import type { SDK } from "caido:plugin"; import { RequestSpec } from "caido:utils"; -const scan = async ( // scan 함수 정의 - sdk: SDK, - url: string -): Promise<{ data: string }> => { - sdk.console.log(`들어온 url : ${url}`); // url이 잘 들어왔는지 확인함요 - - // url이 string이 아니고 , 값이 없거나 그럴 때 유효한 값 넣으라고 출력. - if (!url || typeof url !== "string") { - sdk.console.log("이상한 url 입력함."); - return { data: "알맞은 URL을 입력하세요." }; - } - - try { - const spec = new RequestSpec(url); // url에 GET 요청 보낼거긔. - spec.setMethod("GET"); - spec.setHeader("User-Agent", "Caido Scanner"); - spec.setHeader("Accept", "*/*"); - sdk.console.log(`요청 URL: ${url}`); - - const res = await sdk.requests.send(spec); // 요청 보내고 응답 받음. - sdk.console.log('[SCAN] 응답 :', res); - sdk.console.log(`[SCAN] 요청 성공:${(res as any).status}`); - sdk.console.log(`[SCAN] body: ${(res as any).body ? (res as any).body.toString().substring(0, 100) : "없음"}`); - - const html = (res as any).body ? (res as any).body.toString() : ""; - - // ]*href="([^"]+)"[^>]*>/gi; - const anchors: string[] = []; - let match; - while ((match = anchorRegex.exec(html)) !== null) { // html에서 a href 찾아 배열에 저장함. - if (typeof match[1] === "string") { - anchors.push(match[1]); - } +export class ScopeDetection { + async scan( + sdk: SDK, + url: string + ): Promise<{ data: string }> { + sdk.console.log(`들어온 url : ${url}`); // url이 잘 들어왔는지 확인함요 + + // url이 string이 아니고 , 값이 없거나 그럴 때 유효한 값 넣으라고 출력. + if (!url || typeof url !== "string") { + sdk.console.log("이상한 url 입력함."); + return { data: "알맞은 URL을 입력하세요." }; } - sdk.console.log(`찾아진 a href 개수: ${anchors.length}`); - // 5. scope 탐지 - const results: string[] = []; - anchors.forEach((href) => { // 추출한 a href 링크 하나씩 검사드감. - try { - const absHref = new URL(href, url).href; // 상대경로라면 url 기준으로 절대 URL 바꿔줌줌 - sdk.console.log(`[SCAN] 절대 URL 변환: ${href} -> ${absHref}`); // + try { + const spec = new RequestSpec(url); // url에 GET 요청 보낼거긔. + spec.setMethod("GET"); + spec.setHeader("User-Agent", "Caido Scanner"); + spec.setHeader("Accept", "*/*"); + sdk.console.log(`요청 URL: ${url}`); - if (/oauth|authorize|login|accounts|auth/i.test(absHref)) { // url에 이런 OAuth 키워드가 있는지 필터링. - let u: URL; - try { - u = new URL(absHref); // 필터링된 url을 url 객체로 파싱. 정식 url인 경우 변수 u에 저장. - } catch (err) { // 파싱 실패하면 - sdk.console.log( - `URL 파싱 실패 : ${absHref} (${err instanceof Error ? err.message : err})` - ); - return; - } + const res = await sdk.requests.send(spec); // 요청 보내고 응답 받음. + sdk.console.log('[SCAN] 응답 :', res); + sdk.console.log(`[SCAN] 요청 성공:${(res as any).status}`); + sdk.console.log(`[SCAN] body: ${(res as any).body ? (res as any).body.toString().substring(0, 100) : "없음"}`); - try { - const scope = u.searchParams.get("scope"); // url에 scope있긔?scope값 가져와. - if (scope && /all|\*/i.test(scope)) { // scope가 존재하고 all, *있다면. - results.push(`위험한 scope 발견: ${scope}\n -> ${absHref}`); // results에 경고 메시지 전달. - } - } catch (err) { - sdk.console.log(`searchParams.get 실패`); - } + const html = (res as any).body ? (res as any).body.toString() : ""; + + // ]*href="([^"]+)"[^>]*>/gi; + const anchors: string[] = []; + let match; + while ((match = anchorRegex.exec(html)) !== null) { // html에서 a href 찾아 배열에 저장함. + if (typeof match[1] === "string") { + anchors.push(match[1]); } - } catch (e) { - sdk.console.log( - `URL 파싱 실패 (absHref 단계): ${href} (${e instanceof Error ? e.message : e})` - ); } - }); + sdk.console.log(`찾아진 a href 개수: ${anchors.length}`); - const resultStr = results.join("\n") || "위험한 scope가 발견되지 않았습니다."; - return { data: resultStr }; // 성공했는지 실패했는지 App.vue한테 전달할 메시지. - } catch (e) { - sdk.console.log(`백엔드 에러: ${e instanceof Error ? e.message : e}`); - return { data: "백엔드 에러: " + (e instanceof Error ? e.message : String(e)) }; // App.vue에 전달할 메시지. - } -}; + // 5. scope 탐지 + const results: string[] = []; + anchors.forEach((href) => { // 추출한 a href 링크 하나씩 검사드감. + try { + const absHref = new URL(href, url).href; // 상대경로라면 url 기준으로 절대 URL 바꿔줌줌 + sdk.console.log(`[SCAN] 절대 URL 변환: ${href} -> ${absHref}`); // -export type API = DefineAPI<{ - scan: typeof scan; -}>; + if (/oauth|authorize|login|accounts|auth/i.test(absHref)) { // url에 이런 OAuth 키워드가 있는지 필터링. + let u: URL; + try { + u = new URL(absHref); // 필터링된 url을 url 객체로 파싱. 정식 url인 경우 변수 u에 저장. + } catch (err) { // 파싱 실패하면 + sdk.console.log( + `URL 파싱 실패 : ${absHref} (${err instanceof Error ? err.message : err})` + ); + return; + } -export function init(sdk: SDK) { - sdk.api.register("scan", scan); + try { + const scope = u.searchParams.get("scope"); // url에 scope있긔?scope값 가져와. + if (scope && /all|\*/i.test(scope)) { // scope가 존재하고 all, *있다면. + results.push(`위험한 scope 발견: ${scope}\n -> ${absHref}`); // results에 경고 메시지 전달. + } + } catch (err) { + sdk.console.log(`searchParams.get 실패`); + } + } + } catch (e) { + sdk.console.log( + `URL 파싱 실패 (absHref 단계): ${href} (${e instanceof Error ? e.message : e})` + ); + } + }); + + const resultStr = results.join("\n") || "위험한 scope가 발견되지 않았습니다."; + return { data: resultStr }; // 성공했는지 실패했는지 App.vue한테 전달할 메시지. + } catch (e) { + sdk.console.log(`백엔드 에러: ${e instanceof Error ? e.message : e}`); + return { data: "백엔드 에러: " + (e instanceof Error ? e.message : String(e)) }; // App.vue에 전달할 메시지. + } + }; } \ No newline at end of file diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2eccd6d..244a3e2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -3,12 +3,14 @@ import type { Request } from "caido:utils"; import { ImplicitGrantController } from "./controller/implictGrant"; import { AuthZCodeGrantController } from "./controller/authZCodeGrant"; import { PKCECheck } from "./controller/PKCECheck"; +import { ScopeDetection } from "./controller/scopeDetection"; export type API = DefineAPI<{}>; 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(); @@ -36,6 +38,7 @@ export function init(sdk: SDK) { if (result) { await pkceCheckController.test(sdk, req); + await ScopeDetectionController.scan(sdk, req.getUrl()); await sdk.findings.create({ title: "Possible SSO Request Detected", From 858dfd16dc24600d4bb604ce9e6f457e66129be8 Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sun, 25 May 2025 21:43:21 +0900 Subject: [PATCH 3/7] =?UTF-8?q?FEAT=20:=20AccessToken=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=81=EC=A2=85=20=ED=86=A0=ED=81=B0=20=EC=A1=B4=EC=9E=AC=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20con?= =?UTF-8?q?troller=20=EC=9E=91=EC=84=B1,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/backend/src/controller/accessTokenDetector.ts diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts new file mode 100644 index 0000000..fb3d03f --- /dev/null +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -0,0 +1,146 @@ +import type { Request, Response } from "caido:utils"; + +// 토큰 누출 검사 결과를 담는 구조 +export interface TokenLeakResult { + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) +} + +// 액세스 토큰 누출 검사 클래스 +export class AccessTokenLeakController { + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testReq(request: Request): Promise { + + // === 1. URL에서 토큰 검사 === + const url = request.getUrl(); + + const extractedTokenFromUrl = this.extractTokenFromText(url); + + if (extractedTokenFromUrl) { + return { + found: true, + location: 'url', + title: "Access Token Leak in URL", + description: `요청 URL에 액세스 토큰 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, + value: url + }; + } + + // === 2. 요청 본문(Body)에서 토큰 검사 === + const body = request.getBody(); + + if (body) { + const bodyText = await body.toText(); + + const extractedTokenFromBody = this.extractTokenFromText(bodyText); + + if (extractedTokenFromBody) { + return { + found: true, + location: 'body', + title: "Access Token Leak in Request Body", + description: `요청 Body에 access_token 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + value: bodyText + }; + } + } + + return null; + } + + /** + * HTTP 응답에서 액세스 토큰 누출 검사 + * @param response - 검사할 HTTP 응답 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testResp(response: Response): Promise { + + // === 1. Location 헤더에서 토큰 검사 === + const locationHeader = response.getHeader("Location"); + + const locationHeaderStr = Array.isArray(locationHeader) ? locationHeader.join(', ') : locationHeader; + + if (locationHeaderStr) { + const extractedTokenFromHeader = this.extractTokenFromText(locationHeaderStr); + + if (extractedTokenFromHeader) { + return { + found: true, + location: 'header', + title: "Access Token Leak in Redirect URL", + description: `Location 헤더에 액세스 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, + value: locationHeaderStr + }; + } + } + + // === 2. 응답 본문에서 토큰 검사 === + const bodyBytes = response.getBody(); + + if (bodyBytes) { + const bodyText = await bodyBytes.toText(); + + const extractedTokenFromBody = this.extractTokenFromText(bodyText); + + if (extractedTokenFromBody) { + return { + found: true, + location: 'body', + title: "Access Token Leak in Response Body", + description: `HTTP 응답 본문에 'access_token' 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + value: bodyText + }; + } + } + + return null; + } + + /** + * 텍스트에서 실제 토큰 값을 추출 + * @param text - 검사할 텍스트 + * @returns 토큰 값이 있으면 해당 값, 없으면 null + */ +private extractTokenFromText(text: string): string | null { + // 토큰 관련 키워드 리스트 + const tokenKeys = [ + 'access_token', + 'id_token', + 'auth_token', + 'token', + 'jwt', + 'session_token' + ]; + + // 정규표현식 패턴 리스트 생성 + const tokenPatterns: RegExp[] = []; + + for (const key of tokenKeys) { + // 1. key=token 또는 key: token + tokenPatterns.push(new RegExp(`${key}[=:]\\s*([a-zA-Z0-9\\-._~+/]+=*)`, 'i')); + + // 2. JSON 형태의 "key": "token" + tokenPatterns.push(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`, 'i')); + } + + // 3. Authorization: Bearer 형태 + tokenPatterns.push(/bearer\s+([a-zA-Z0-9\-._~+/]+=*)/i); + + // 모든 패턴에 대해 검사 + for (const pattern of tokenPatterns) { + const match = pattern.exec(text); + if (match && match[1]) { + return match[1]; + } + } + + return null; + } +} \ No newline at end of file From 7b704cacf499a68cbc7a4d2cae058bca19d579af Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sat, 31 May 2025 11:55:44 +0900 Subject: [PATCH 4/7] =?UTF-8?q?STYLE=20:=20=EB=A1=9C=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts index fb3d03f..22be16e 100644 --- a/packages/backend/src/controller/accessTokenDetector.ts +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -2,21 +2,21 @@ import type { Request, Response } from "caido:utils"; // 토큰 누출 검사 결과를 담는 구조 export interface TokenLeakResult { - found: boolean; // 토큰이 발견되었는지 여부 (true/false) - location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) - title: string; // 경고 제목 - description: string; // 상세 설명 - value?: string; // 실제 발견된 값 (선택적) + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) } // 액세스 토큰 누출 검사 클래스 export class AccessTokenLeakController { - - /** - * @param request - 검사할 HTTP 요청 객체 - * @returns 토큰이 발견되면 결과 객체, 없으면 null - */ - async testReq(request: Request): Promise { + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async testReq(request: Request): Promise { // === 1. URL에서 토큰 검사 === const url = request.getUrl(); @@ -28,7 +28,7 @@ export class AccessTokenLeakController { found: true, location: 'url', title: "Access Token Leak in URL", - description: `요청 URL에 액세스 토큰 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, + description: `요청 URL에 토큰이 포함되어 있습니다. (토큰: ${extractedTokenFromUrl.substring(0, 20)}...)`, value: url }; } @@ -46,7 +46,7 @@ export class AccessTokenLeakController { found: true, location: 'body', title: "Access Token Leak in Request Body", - description: `요청 Body에 access_token 파라미터가 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + description: `요청 Body에 토큰이이 포함되어 있습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; } @@ -75,7 +75,7 @@ export class AccessTokenLeakController { found: true, location: 'header', title: "Access Token Leak in Redirect URL", - description: `Location 헤더에 액세스 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, + description: `Location 헤더에 토큰이 노출되었습니다: ${locationHeaderStr} (토큰: ${extractedTokenFromHeader.substring(0, 20)}...)`, value: locationHeaderStr }; } @@ -88,13 +88,13 @@ export class AccessTokenLeakController { const bodyText = await bodyBytes.toText(); const extractedTokenFromBody = this.extractTokenFromText(bodyText); - + if (extractedTokenFromBody) { return { found: true, location: 'body', title: "Access Token Leak in Response Body", - description: `HTTP 응답 본문에 'access_token' 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, + description: `HTTP 응답 본문에 토큰이 노출되었습니다. (토큰: ${extractedTokenFromBody.substring(0, 20)}...)`, value: bodyText }; } From d9353220e64867cdb1006fd37a77a8dac467365b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=94=EB=83=A5=20=28imnyang=29?= Date: Sat, 31 May 2025 12:03:49 +0900 Subject: [PATCH 5/7] Update README.md --- README.md | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c5497cc..90fb3a9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,6 @@ # caido-plugin-test -## To-Do -- [ ] PKCE 다운그래이드 https에서 작동 안하는 이슈 고치기 - -```log -2025-05-25T15:52:40.757475Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connection (29e74afd-9006-445e-88a9-3fc5d4796af9) -2025-05-25T15:52:40.757530Z INFO actix-rt|system:0|arbiter:6 proxy|connect: Client connected for http://localhost:8787 (29e74afd-9006-445e-88a9-3fc5d4796af9) -2025-05-25T15:52:40.757562Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost/login (29e74afd-9006-445e-88a9-3fc5d4796af9) -2025-05-25T15:52:40.767186Z INFO actix-rt|system:0|arbiter:6 proxy|http1|logger: GET http://localhost:8787/login -> 302 361 (29e74afd-9006-445e-88a9-3fc5d4796af9) -2025-05-25T15:52:40.768696Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b) -2025-05-25T15:52:41.103596Z INFO actix-rt|system:0|arbiter:9 proxy|http1|logger: GET https://github.com/login/oauth/authorize?client_id=Ov23lixietSCQOHxPvcr&redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback&scope=read%3Auser&state=bc11db571a4737d0&response_type=code&code_challenge=FtSdQsWI342PKH6BGgKYR6AOzW95LaS0jeVcwTmHaro&code_challenge_method=S256 -> 302 4927 (90f314dc-9480-4bd8-b7b6-5acba6b8bc7b) -2025-05-25T15:52:41.105944Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connection (34585a00-9f9f-4c72-b087-2e9e92418dad) -2025-05-25T15:52:41.105993Z INFO actix-rt|system:0|arbiter:7 proxy|connect: Client connected for http://localhost:8787 (34585a00-9f9f-4c72-b087-2e9e92418dad) -2025-05-25T15:52:41.106023Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 (34585a00-9f9f-4c72-b087-2e9e92418dad) -2025-05-25T15:52:41.108270Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] Required PKCE parameters missing. Skipping. -2025-05-25T15:52:41.277387Z INFO plugin:65ad3a87-0257-4408-a9c7-e0885e04c162 js|sdk: [PKCEDowngradeCheck] No PKCE downgrade detected. -2025-05-25T15:52:41.686109Z INFO actix-rt|system:0|arbiter:7 proxy|http1|logger: GET http://localhost:8787/callback?code=10c34dcc4d3f7302e707&state=bc11db571a4737d0 -> 200 1582 (34585a00-9f9f-4c72-b087-2e9e92418dad) -``` \ No newline at end of file +```bash +pnpm install +pnpm run watch +``` From f1b5ef5f9b668d57a2c9999b34e142531bc8afac Mon Sep 17 00:00:00 2001 From: KMINGON Date: Sat, 31 May 2025 12:37:54 +0900 Subject: [PATCH 6/7] =?UTF-8?q?REFACTOR=20:=20findings=EB=A5=BCindex?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EB=AA=A8=EB=93=88=EC=95=A0?= =?UTF-8?q?=EC=84=9C=20=EB=A7=8C=EB=93=A4=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controller/accessTokenDetector.ts | 48 ++++++++++++++----- packages/backend/src/index.ts | 4 ++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/controller/accessTokenDetector.ts b/packages/backend/src/controller/accessTokenDetector.ts index 22be16e..8093a54 100644 --- a/packages/backend/src/controller/accessTokenDetector.ts +++ b/packages/backend/src/controller/accessTokenDetector.ts @@ -1,22 +1,46 @@ import type { Request, Response } from "caido:utils"; +import type { SDK, DefineAPI } from "caido:plugin"; // 토큰 누출 검사 결과를 담는 구조 export interface TokenLeakResult { - found: boolean; // 토큰이 발견되었는지 여부 (true/false) - location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) - title: string; // 경고 제목 - description: string; // 상세 설명 - value?: string; // 실제 발견된 값 (선택적) + found: boolean; // 토큰이 발견되었는지 여부 (true/false) + location: 'url' | 'body' | 'header'; // 토큰이 발견된 위치 (url, body, header 중 하나) + title: string; // 경고 제목 + description: string; // 상세 설명 + value?: string; // 실제 발견된 값 (선택적) } // 액세스 토큰 누출 검사 클래스 export class AccessTokenLeakController { - - /** - * @param request - 검사할 HTTP 요청 객체 - * @returns 토큰이 발견되면 결과 객체, 없으면 null - */ - async testReq(request: Request): Promise { + async testReq(sdk: SDK>, request: Request): Promise { + const result = await this._scanRequest(request); + if (result) { + await sdk.findings.create({ + title: result.title, + description: result.description, + request, + reporter: "", + }); + } + } + + async testResp(sdk: SDK>, response: Response, request: Request): Promise { + const result = await this._scanResponse(response); + if (result) { + await sdk.findings.create({ + title: result.title, + description: result.description, + request, + reporter: "", + }); + } + } + + /** + * @param request - 검사할 HTTP 요청 객체 + * @returns 토큰이 발견되면 결과 객체, 없으면 null + */ + async _scanRequest(request: Request): Promise { // === 1. URL에서 토큰 검사 === const url = request.getUrl(); @@ -60,7 +84,7 @@ export class AccessTokenLeakController { * @param response - 검사할 HTTP 응답 객체 * @returns 토큰이 발견되면 결과 객체, 없으면 null */ - async testResp(response: Response): Promise { + async _scanResponse(response: Response): Promise { // === 1. Location 헤더에서 토큰 검사 === const locationHeader = response.getHeader("Location"); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a24d2c7..9cf32b2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -4,6 +4,7 @@ import type { Request, Response } from "caido:utils"; // import { AuthZCodeGrantController } from "./controller/authZCodeGrant"; import { CsrfCheck } from "./controller/csrfCheck"; import { PKCECheck } from "./controller/PKCECheck"; +import { AccessTokenLeakController } from "./controller/accessTokenDetector"; export type API = DefineAPI<{}>; @@ -11,6 +12,7 @@ const csrfCheck = new CsrfCheck(); // const implicitGrantController = new ImplicitGrantController(); // const authZCodeGrantController = new AuthZCodeGrantController(); const pkceCheckController = new PKCECheck(); +const tokenCheck = new AccessTokenLeakController(); export function init(sdk: SDK) { // sdk.events.onInterceptRequest(async (sdk, req: Request) => { @@ -30,6 +32,8 @@ export function init(sdk: SDK) { async (sdk: SDK, {}>, 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); // sdk.events.onInterceptRequest(async (sdk, req: Request) => { // const result = // authZCodeGrantController.testReq(req) || From 907fcd81208c07990f8a2b371abcda1814345d68 Mon Sep 17 00:00:00 2001 From: imnyang Date: Sat, 31 May 2025 15:02:27 +0900 Subject: [PATCH 7/7] Remove pkce --- .gitignore | 3 +- dist/plugin_package.zip | Bin 15658 -> 0 bytes playground/pkce/.gitignore | 34 -------------------- playground/pkce/README.md | 15 --------- playground/pkce/bun.lock | 25 -------------- playground/pkce/package.json | 10 ------ playground/pkce/src/PKCEDowngradeExpress.js | 31 ------------------ playground/pkce/tsconfig.json | 29 ----------------- 8 files changed, 2 insertions(+), 145 deletions(-) delete mode 100644 dist/plugin_package.zip delete mode 100644 playground/pkce/.gitignore delete mode 100644 playground/pkce/README.md delete mode 100644 playground/pkce/bun.lock delete mode 100644 playground/pkce/package.json delete mode 100644 playground/pkce/src/PKCEDowngradeExpress.js delete mode 100644 playground/pkce/tsconfig.json diff --git a/.gitignore b/.gitignore index 648628f..0d4515a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,linux diff --git a/dist/plugin_package.zip b/dist/plugin_package.zip deleted file mode 100644 index 28184677ccc0929e907d8d2e3ba3431c8285501f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15658 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 diff --git a/playground/pkce/.gitignore b/playground/pkce/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/playground/pkce/.gitignore +++ /dev/null @@ -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 diff --git a/playground/pkce/README.md b/playground/pkce/README.md deleted file mode 100644 index 4a3109f..0000000 --- a/playground/pkce/README.md +++ /dev/null @@ -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. diff --git a/playground/pkce/bun.lock b/playground/pkce/bun.lock deleted file mode 100644 index 0a70737..0000000 --- a/playground/pkce/bun.lock +++ /dev/null @@ -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=="], - } -} diff --git a/playground/pkce/package.json b/playground/pkce/package.json deleted file mode 100644 index 0bbbfb8..0000000 --- a/playground/pkce/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "playground", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/playground/pkce/src/PKCEDowngradeExpress.js b/playground/pkce/src/PKCEDowngradeExpress.js deleted file mode 100644 index 61cf737..0000000 --- a/playground/pkce/src/PKCEDowngradeExpress.js +++ /dev/null @@ -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}`); -}); diff --git a/playground/pkce/tsconfig.json b/playground/pkce/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/playground/pkce/tsconfig.json +++ /dev/null @@ -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 - } -}