// app.js const express = require("express"); const crypto = require("crypto"); const app = express(); const port = 8000; // 콜백 엔드포인트 (정상 동작 시뮬레이션) app.get("/callback", (req, res) => { res.send(`
Query Params:
${JSON.stringify(req.query, null, 2)}
`);
});
/**
* 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";
// 클라이언트 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}`);
});
/**
* 3) 랜덤 state를 생성하여 리다이렉트를 발생시키는 테스트용 엔드포인트
* - /authorize/reuse-state-test 로 접근할 때마다 새로운 16진수 state를 생성
* - 최초 요청에 OAuth 파라미터가 없으므로 isOauthUri(request) == false
* - 응답에 Location 헤더로 '...?state=<랜덤값>' 을 포함
* -> Caido 플러그인의 checkNonceReuse 로직에서 새로운 state가 발급되었는지,
* 재사용되었는지를 검증할 수 있음
* - 더하여 callback uri에서 해당 nonce의 유효성을 판단하지 않고 응답 시에 vuln
*/
app.get("/authorize/reuse-state-test", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
// 고정된 콜백 URI로 리다이렉트 (OAuth 파라미터는 여기서만 주입)
const location = `http://localhost:${port}/callback?state=${state}&client_id=123`;
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`
);
console.log(
`3) Reuse-State-Test: http://localhost:${port}/authorize/reuse-state-test`
);
});