diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b46d5d..9071b63 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,15 +25,8 @@ jobs: run: | bun run build - - name: Archive built plugin - run: | - mkdir -p dist-artifact - cp -r dist/* dist-artifact/ - # 만약 manifest.json도 포함되어야 한다면 - cp manifest.json dist-artifact/ - - name: Upload plugin artifact uses: actions/upload-artifact@v4 with: name: caido-plugin - path: dist-artifact + path: dist diff --git a/packages/backend/src/controller/PKCECheck.ts b/packages/backend/src/controller/PKCECheck.ts new file mode 100644 index 0000000..3e41154 --- /dev/null +++ b/packages/backend/src/controller/PKCECheck.ts @@ -0,0 +1,98 @@ +import type { SDK } from "caido:plugin"; +import type { Request } from "caido:utils"; +import { fetch, Request as FetchRequest } from "caido:http"; + +export class PKCECheck { + async test(sdk: SDK, req: Request): Promise { + const method = req.getMethod(); + if (method !== "GET") { + sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping."); + return false; + } + + const query = req.getQuery(); + const requiredParams = ["client_id=", "response_type=code", "code_challenge=", "code_challenge_method="]; + if (!requiredParams.every(param => query.includes(param))) { + sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping."); + return false; + } + + const url = req.getUrl(); + const isOpenID = query.includes("scope=openid") || query.includes("id_token"); + const methodMatch = query.match(/code_challenge_method=([^&]*)/); + const challengeMatch = query.match(/code_challenge=([^&]*)/); + + if (!methodMatch || !challengeMatch) { + sdk.console.log("[PKCEDowngradeCheck] code_challenge or method missing. Skipping."); + await sdk.findings.create({ + title: isOpenID + ? "[WARN] OpenID Flow PKCE Parameters Missing" + : "[WARN] OAuth2 Flow PKCE Parameters Missing", + description: `PKCE parameters are missing or incomplete for ${url}. This may indicate a misconfiguration.`, + request: req, + reporter: "", + }); + return false; + } + + const methodVal = decodeURIComponent(methodMatch[1]!); + if (methodVal === "plain") { + sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is 'plain'. Skipping."); + await sdk.findings.create({ + title: isOpenID + ? "[WARN] OpenID Flow PKCE Method is 'plain'" + : "[WARN] OAuth2 Flow PKCE Method is 'plain'", + description: `PKCE method is set to 'plain' for ${url}. This may indicate a downgrade vulnerability.`, + request: req, + reporter: "", + }); + + return false; + } + + const downgradedQuery = query + .replace(/code_challenge_method=[^&]*&?/, "") + .replace(/code_challenge=[^&]*&?/, "") + .replace(/[?&]$/, ""); + + const downgradedUrl = `${req.getUrl().split("://")[0]}://${req.getHost()}:${req.getPort()}${req.getPath()}?${downgradedQuery}`; + + try { + const [resOriginal, resDowngraded] = await Promise.all([ + fetch(new FetchRequest(url, { method: "GET" })), + fetch(new FetchRequest(downgradedUrl, { method: "GET" })) + ]); + + const [bodyOriginal, bodyDowngraded] = await Promise.all([ + resOriginal.text(), + resDowngraded.text() + ]); + + const statusEqual = resOriginal.status === resDowngraded.status; + const codeInBoth = bodyOriginal.includes("code=") && bodyDowngraded.includes("code="); + + if (statusEqual && codeInBoth) { + const title = isOpenID + ? "[CRITICAL] OpenID Flow PKCE Downgraded to Plaintext" + : "[CRITICAL] OAuth2 Flow PKCE Downgraded to Plaintext"; + const reference = isOpenID + ? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7" + : "https://datatracker.ietf.org/doc/html/rfc7636"; + + await sdk.findings.create({ + title, + description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nReference: ${reference}`, + request: req, + reporter: "", + }); + + return true; + } + } catch (err) { + sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(err)}`); + } + + sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected."); + return false; + } +} diff --git a/packages/backend/src/controller/PKCEDowngradeCheck.ts b/packages/backend/src/controller/PKCEDowngradeCheck.ts deleted file mode 100644 index 5d9e5e4..0000000 --- a/packages/backend/src/controller/PKCEDowngradeCheck.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { SDK } from "caido:plugin"; -import type { Request, Response } from "caido:utils"; -import { fetch, Request as FetchRequest } from "caido:http"; - -export class PKCEDowngradeCheck { - async test(sdk: SDK, req: Request): Promise { - const method = req.getMethod(); - const query = req.getQuery(); - - sdk.console.log(`[PKCEDowngradeCheck] Method: ${method}`); - sdk.console.log(`[PKCEDowngradeCheck] Query: ${query}`); - - if (method !== "GET") { - sdk.console.log("[PKCEDowngradeCheck] Not a GET request. Skipping."); - return false; - } - - if ( - !query.includes("client_id=") || - !query.includes("response_type=code") || - !query.includes("code_challenge=") || - !query.includes("code_challenge_method=") - ) { - sdk.console.log("[PKCEDowngradeCheck] Required PKCE parameters missing. Skipping."); - return false; - } - - const url = req.getUrl(); - const isOpenID = - query.includes("scope=openid") || query.includes("id_token"); - - sdk.console.log(`[PKCEDowngradeCheck] URL: ${url}`); - sdk.console.log(`[PKCEDowngradeCheck] isOpenID: ${isOpenID}`); - - const methodMatch = query.match(/code_challenge_method=([^&]*)/); - const challengeMatch = query.match(/code_challenge=([^&]*)/); - if (!methodMatch || !challengeMatch) { - sdk.console.log("[PKCEDowngradeCheck] code_challenge or code_challenge_method missing in query. Skipping."); - return false; - } - - const methodVal = decodeURIComponent(methodMatch[1] ?? ""); - sdk.console.log(`[PKCEDowngradeCheck] code_challenge_method: ${methodVal}`); - if (methodVal === "plain") { - sdk.console.log("[PKCEDowngradeCheck] code_challenge_method is plain. Skipping."); - return false; - } - - const modifiedQuery = query - .replace(/code_challenge_method=[^&]*&?/, "") - .replace(/code_challenge=[^&]*&?/, "") - .replace(/[?&]$/, ""); - - const downgradedUrl = `${req.getUrl().split("://")[0]}://${req.getHost()}:${req.getPort()}${req.getPath()}?${modifiedQuery}`; - sdk.console.log(`[PKCEDowngradeCheck] Downgraded URL: ${downgradedUrl}`); - - try { - const fetchOriginal = new FetchRequest(url, { method: "GET" }); - const fetchDowngraded = new FetchRequest(downgradedUrl, { method: "GET" }); - - sdk.console.log("[PKCEDowngradeCheck] Sending original request..."); - const resOriginal = await fetch(fetchOriginal); - sdk.console.log(`[PKCEDowngradeCheck] Original response status: ${resOriginal.status}`); - - sdk.console.log("[PKCEDowngradeCheck] Sending downgraded request..."); - const resDowngraded = await fetch(fetchDowngraded); - sdk.console.log(`[PKCEDowngradeCheck] Downgraded response status: ${resDowngraded.status}`); - - const statusEqual = resOriginal.status === resDowngraded.status; - sdk.console.log(`[PKCEDowngradeCheck] Status equal: ${statusEqual}`); - - const bodyOriginal = await resOriginal.text(); - const bodyDowngraded = await resDowngraded.text(); - - const codeInOriginal = bodyOriginal.includes("code="); - const codeInDowngrade = bodyDowngraded.includes("code="); - - sdk.console.log(`[PKCEDowngradeCheck] code= in original: ${codeInOriginal}`); - sdk.console.log(`[PKCEDowngradeCheck] code= in downgraded: ${codeInDowngrade}`); - - if (statusEqual && codeInOriginal && codeInDowngrade) { - const title = isOpenID - ? "OpenID Flow PKCE Downgraded to Plaintext" - : "OAuth2 Flow PKCE Downgraded to Plaintext"; - - const reference = isOpenID - ? "https://openid.net/specs/openid-igov-oauth2-1_0-02.html#rfc.section.3.1.7" - : "https://datatracker.ietf.org/doc/html/rfc7636"; - - sdk.console.log(`[PKCEDowngradeCheck] PKCE downgrade detected! Creating finding.`); - - await sdk.findings.create({ - title, - description: `PKCE downgrade detected for ${url}.\n\nDowngraded URL: ${downgradedUrl}\n\nReference: ${reference}`, - request: req, - reporter: "", - }); - - return true; - } - } catch (e) { - sdk.console.error(`PKCE downgrade check failed for ${url}: ${String(e)}`); - } - - sdk.console.log("[PKCEDowngradeCheck] No PKCE downgrade detected."); - return false; - } -} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 8eafcc4..7633932 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -2,13 +2,13 @@ import type { SDK, DefineAPI } from "caido:plugin"; import type { Request } from "caido:utils"; import { ImplicitGrantController } from "./controller/implictGrant"; import { AuthZCodeGrantController } from "./controller/authZCodeGrant"; -import { PKCEDowngradeCheck } from "./controller/PKCEDowngradeCheck"; +import { PKCECheck } from "./controller/PKCECheck"; export type API = DefineAPI<{}>; const implicitGrantController = new ImplicitGrantController(); const authZCodeGrantController = new AuthZCodeGrantController(); -const pkceDowngradeCheck = new PKCEDowngradeCheck(); +const pkceCheck = new PKCECheck(); // function matchSSORequest(req: Request): boolean { // const raw = req.getRaw().toString(); @@ -35,7 +35,7 @@ export function init(sdk: SDK) { implicitGrantController.testReq(req); if (result) { - await pkceDowngradeCheck.test(sdk, req); + await pkceCheck.test(sdk, req); await sdk.findings.create({ title: "Possible SSO Request Detected",