awesome-everything EN
↑ Обратно к восхождению

Безопасность

OAuth/OIDC: код-ревью

Суть Читай реальный код OAuth/OIDC — обмен PKCE, валидацию id_token, ротацию refresh token и привязку DPoP — находи пропущенную или вывернутую обязательную проверку и выбирай фикс.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Каждая CVE в OAuth — это одна пропущенная или вывернутая проверка, спрятанная в коде, который выглядит верным. Читай каждый сниппет как security-ревьюер, находи пропущенный обязательный шаг и выбирай фикс, который закрывает реальную дыру.

Цель

Отработай цикл ревью, который ловит баги OAuth до выката: читай путь обмена или валидации, определи, какая обязательная проверка отсутствует или неверна, и применяй точный фикс, а не размытое «усиление».

Сниппет 1 — обмен токенов

// callback handler after the IdP redirects back with ?code=...&state=...
async function handleCallback(req, session) {
  const { code, state } = req.query;
  const res = await fetch(TOKEN_ENDPOINT, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      code_verifier: session.codeVerifier,
    }),
  });
  return res.json(); // tokens
}
Викторина

PKCE code_verifier передан верно. Какая обязательная проверка отсутствует в этом callback и что она позволяет атакующему?

Сниппет 2 — валидация id_token

import jwt from "jsonwebtoken";

function verifyIdToken(idToken, jwks) {
  const header = decodeHeader(idToken);
  const key = jwks.find((k) => k.kid === header.kid);
  // verify using whatever the token says it used
  const claims = jwt.verify(idToken, key.pem, { algorithms: [header.alg] });
  if (claims.iss !== EXPECTED_ISS) throw new Error("bad iss");
  if (claims.exp < Date.now() / 1000) throw new Error("expired");
  return claims;
}
Викторина

Этот валидатор проверяет iss и exp. Какие две обязательные проверки сломаны или отсутствуют и почему это важно?

Сниппет 3 — ротация refresh

async function refresh(presentedRT) {
  const record = await db.refreshTokens.find(presentedRT);
  if (!record) throw new Error("unknown refresh token");
  // issue a fresh pair
  const next = await mintTokens(record.userId);
  await db.refreshTokens.insert(next.refreshToken, { userId: record.userId });
  return next;
}
Викторина

Это выпускает новый refresh token на каждый вызов. Почему оно не обнаруживает украденный refresh token и каков фикс?

Сниппет 4 — проверка DPoP proof

function verifyDpop(proofJwt, accessToken, req) {
  const { header, payload } = parse(proofJwt);
  verifySignature(proofJwt, header.jwk); // signed by embedded key
  if (payload.htm !== req.method) throw new Error("htm");
  if (payload.htu !== req.url) throw new Error("htu");
  if (Math.abs(now() - payload.iat) > 60) throw new Error("iat skew");
  return true;
}
Викторина

Это проверяет подпись proof, htm, htu и iat. Каких двух привязок DPoP не хватает и какую атаку открывает каждый пробел?

Итог

Каждый сниппет ломался так же, как ломается реальный код OAuth — пропуском или выворачиванием одной обязательной проверки при верном виде: отсутствие сравнения state открывает login CSRF; доверие к header.alg и пропуск aud/nonce открывают algorithm-confusion, audience-confusion и replay; выпуск без пометки об использовании убивает ротацию; а DPoP-валидатор без ath, совпадения thumbprint cnf.jkt и кэша jti от replay на деле не sender-constrained. Проверяй полный набор: читай путь, назови отсутствующую проверку, примени точный фикс.

Продолжить восхождение ↑OAuth/OIDC: собери и проверь защищённый логин
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.