불완전한 URL 부분 문자열 검증

Incomplete URL Substring Sanitization

설명

URL을 단순 문자열로 다루며 허용 도메인이 포함되어 있는지(예: includes, indexOf, 단순 endsWith)만 확인하면, 공격자는 서브도메인, 사용자정보(@), 경로/쿼리 문자열 등에 허용 도메인 문자열을 끼워 넣어 검증을 우회할 수 있습니다. 그 결과 사용자를 외부 악성 사이트로 리다이렉트(Open Redirect)하거나, 서버가 내부망으로 요청을 보내게 만드는 SSRF로 악용될 수 있습니다. 예: https://example.com.attacker.io, http://example.com@attacker.io, https://attacker.io/?next=https://example.com 등.

잠재적 영향

  • Open Redirect(오픈 리다이렉트): 피해자를 피싱 사이트로 유도하거나 OAuth redirect_uri 검증을 우회해 세션/토큰 탈취로 이어질 수 있습니다.
  • SSRF(Server-Side Request Forgery): 서버가 내부망(예: 169.254.169.254 메타데이터, 사내 API)으로 요청을 보내도록 유도되어 정보 유출, 포트 스캔이 가능해집니다.
  • 데이터 유출: 서버가 민감한 자원에 접근해 응답을 외부로 전달할 가능성이 있습니다.
  • 보안 정책 우회: 허용 도메인 기반의 리다이렉트/프록시 정책을 무력화하여 접근 제어를 우회할 수 있습니다.

해결 방법

  • URL 파싱 후 검증: new URL(url).hostname으로 호스트명을 추출하고, 명시적 Allowlist와 정확히 일치 비교하세요.
  • 서브도메인 허용 시 라벨 경계 강제: host === 'example.com' || host.endsWith('.example.com') 형태로 경계를 검사하세요. 모든 하위 도메인을 신뢰할 수 있을 때만 이 범위를 허용하세요. 단순 endsWith('example.com')은 evil-example.com 같은 다른 도메인을 통과시킬 수 있어 금지합니다.
  • 스킴/포트 검증: protocol이 'https:'인지 확인하고, 필요 시 포트도 제한하세요.
  • 상대 리다이렉트 권장: 가능하면 서버에서 지정한 내부 경로만 선택하도록 하세요. //external-host 같은 네트워크 경로 참조는 거부하세요.
  • SSRF 방어(서버 outbound 요청 시): 실제 연결하는 IPv4·IPv6 주소의 루프백·사설·링크 로컬·메타데이터 대역을 제한하고, DNS 변경과 리다이렉트를 통한 우회도 차단하세요. 요청 시간과 응답 크기도 제한하세요.
  • 부분 문자열 검증 금지: includes, indexOf, 정규식의 불완전한 패턴 등 부분 일치 기반 검증을 사용하지 마세요.

예시

리다이렉트 대상의 HTTPS 스킴과 도메인 경계를 비교하는 예시입니다. example.com의 모든 하위 도메인을 신뢰한다는 전제이며, 포트 정책과 서버 측 요청의 DNS·IP 검증은 별도로 적용해야 합니다.

변경 전

javascript
const express = require('express');
const app = express();

// BAD: 부분 문자열 검사로 허용 여부를 판단
app.get('/go', (req, res) => {
  const dest = req.query.dest || '/';
  // "example.com"이 문자열에 포함되면 허용 (취약)
  if (dest.includes('example.com')) {
    return res.redirect(dest);
  }
  res.redirect('/');
});

app.listen(3000);

변경 후

javascript
const express = require('express');
const app = express();

// 안전한: URL 파싱 후 호스트 정확 비교 + 라벨 경계 보장 + 스킴 검증
const ALLOWED_HOST = 'example.com';

app.get('/go', (req, res) => {
  const dest = req.query.dest;
  if (!dest) return res.redirect('/');

  let parsed;
  try {
    parsed = new URL(dest);
  } catch (e) {
    return res.redirect('/'); // 잘못된 URL은 차단
  }

  const host = parsed.hostname.toLowerCase();
  const protocolOk = parsed.protocol === 'https:'; // 필요 시 https만 허용
  const hostOk = host === ALLOWED_HOST || host.endsWith('.' + ALLOWED_HOST);

  if (protocolOk && hostOk) {
    return res.redirect(parsed.toString());
  }
  return res.redirect('/');
});

app.listen(3000);

설명:

  • 변경 전: includes 같은 부분 문자열 검사는 공격자가 허용 도메인 문자열을 서브도메인(example.com.attacker.io), 사용자정보(example.com@attacker.io), 경로/쿼리(/?next=https://example.com) 등에 넣어 우회할 수 있습니다. 이 예시에서는 악성 사이트로 리다이렉트될 수 있습니다. 서버 측 요청에 같은 검증을 사용하면 SSRF로 이어질 수 있습니다.
  • 변경 후: new URL로 파싱한 뒤 hostname을 추출해 정확한 Allowlist 매칭을 수행하고, 서브도메인만 허용하도록 라벨 경계를 강제했습니다(host === 'example.com' || host.endsWith('.example.com')). 또한 스킴이 https:인지 확인하고, 조건을 만족하지 않으면 안전한 기본 경로로 리다이렉트하여 우회를 차단합니다.

참조