3. 오픈 리디렉트

(1) 오픈 리디렉트의 구조

1) 이해

오픈 리디렉트(Open Redirect)웹 애플리케이션이 사용자를 신뢰할 수 없는 외부 사이트로 리디렉션하는 공격 방식이다. 애플리케이션에서 URL 리디렉션¹⁾ 사용하고, 그 리디렉트 목적지를 https://www.trusted-site.com/redirect?url=https://www.phishing-site.com와 같이 매개변수로 처리할 때, 공격자는 매개변수로 스팸 URL을 삽입하여 사용자를 악성 사이트로 유도할 수 있다. 이 방식은 애플리케이션이 리디렉트의 매개변수를 검증하지 않아 공격자가 사용자를 외부 URL로 유도할 수 있다는 취약점을 노린다.

2) 공격 시나리오 예시

OAuth의 콜백 URL 악용 시나리오
OAuth의 콜백 URL 악용 시나리오
  1. OAuth를 이용한 소셜 로그인 기능을 제공하는 웹 사이트에서, 인증 후 사용자를 원래 페이지로 돌려보내기 위해 https://app.com/oauth/callback?redirect=https://app.com/home 구조의 콜백 URL을 사용함
  1. 공격자는 이 콜백 URL이 검증되지 않는다는 취약점을 발견하고, XSS 공격을 하는 피싱 사이트 https://phishing-site.com을 제작함
  1. 공격자는 기존 콜백 URL의 매개변수에 피싱 사이트 주소를 넣어 https://app.com/oauth/callback?redirect=https://phishing-site.com라는 악성 URL을 생성함
  1. 그리고 위 URL을 알아보기 어렵도록 인코딩함
      • 퍼센트 인코딩 예시: https://app.com/oauth/callback?redirect=https%3A%2F%2Fphishing-site.com
      • 축약 예시: https://bit.ly/aPP123
  1. 공격자는 피해자에게 악성 URL이 포함된 이메일을 발송함
  1. 피해자가 이메일을 열어 링크를 클릭하면, https://app.com이트로 연결되었다가 곧바로 피싱 사이트에 리디렉션됨
  1. XSS 공격으로 인해 피해자의 브라우저가 악성 스크립트를 실행하고 PC는 보안 위협에 빠짐
 

(2) 대책 방법

오픈 리디렉트 취약점은 외부에서 입력한 파라미터를 URL로 사용해 페이지를 이동하면서 발생한다. 그러므로 오픈 리디렉트를 방어하기 위해서는 리디렉션에 사용되는 URL을 검증하고, 안전한 리디렉션을 구현하는 과정이 필요하다.

1) 화이트리스트(Whitelist) 사용

안전한 도메인들의 목록(Whitelist)을 만들고, 리디렉션 대상 URL이 목록에 포함되는지 확인한 후 리디렉션하는 방법이다. 리디렉션 도메인 목록을 특정할 수 있는 경우 사용하며, 화이트리스트를 지속적으로 관리해야 한다는 단점이 있다.
const safeDomains = ['safe1.com', 'safe2.com', 'safe3.com']; // 화이트리스트 function isUrlSafe(url) { try { const domain = new URL(url).hostname; return safeDomains.some(safeDomain => domain.endsWith(`.${safeDomain}`)); } catch (error) { console.error('Invalid URL:', error); return false; } } function safeRedirect(url) { if (isUrlSafe(url)) { window.location.href = url; } else { alert('Unsafe redirect👿'); } }

2) 정규표현식을 이용한 URL 패턴 검사

정규표현식으로 URL의 구조와 내용을 검증 후 리디렉션하는 방법이다. 화이트리스트보다 유연하지만, 정규표현식에 따라서 URL 보안 수준이 결정되므로 완벽한 차단이 어려울 수 있다.
function isValidUrl(url) { const urlPattern = /^https:\/\/([\w-]+\.)*example\.com\/(?!admin).*/; // https://로 시작하고, 도메인이 example.com이고, '/admin'으로 시작하지 않는 경로만 허용 return urlPattern.test(url); }

3) 간접 리디렉션

사용자가 입력한 URL로 직접 리디렉션하지 않고, 중간 단계를 거치도록 하는 방법이다. 중간 페이지에 계속 같은 버튼을 추가하여 사용자 확인 절차를 넣는 경우도 있지만, 사용자 경험을 저하시킬 수 있다.
<p>외부 사이트로 이동합니다: ${url}</p> <button onclick="proceedRedirect()">계속</button>

4) URL 구조 분석

URL을 프로토콜, 도메인, 경로 등의 구성 요소로 분해하여 각 부분을 개별적으로 검사하는 방법이다. URL 검증시 보다 세밀한 제어가 필요할 때 사용한다.
function analyzeUrl(url) { try { const parsedUrl = new URL(url); // 프로토콜 검사: HTTPS만 허용 if (parsedUrl.protocol !== 'https:') { throw new Error('Only HTTPS protocol is allowed'); } // 도메인 검사: 특정 도메인만 허용 if (!parsedUrl.hostname.endsWith('.trusted-domain.com')) { throw new Error('Domain not allowed'); } // 경로 검사: 특정 경로 차단 if (parsedUrl.pathname.includes('/admin')) { throw new Error('Access to admin paths not allowed'); } return true; } catch (error) { console.error('URL analysis failed:', error.message); return false; } }

5) 서버 측 식별자 사용

URL 파라미터 대신 서버 측에서 관리하는 식별자를 사용해 리디렉션하는 방법이다. 클라이언트 측에서 실제 URL을 노출하지 않아 보안성이 높아지지만, 추가적인 서버 요청이 발생하므로 네트워크 비용이 증가할 수 있다.
  • 클라이언트 측 설정
    • <a href="#" onclick="safeRedirect('abc123')">이동</a> <!-- redirectId = 'abc123' -->
      async function safeRedirect(redirectId) { try { const response = await fetch(`/api/getRedirectURL/${redirectId}`); if (!response.ok) { throw new Error('Failed to fetch redirect URL'); } const { url } = await response.json(); window.location.href = url; // 서버에서 반환한 URL로 리디렉션 } catch (error) { alert('Unsafe redirect👿'); } }
  • 서버 측 설정
    • const express = require('express'); const app = express(); // 서버 앱 생성 const redirectMap = { 'home': 'https://app.com', 'profile': 'https://app.com/profile', // ... }; app.get('/api/getRedirectURL/:id', (req, res) => { // '/api/getRedirectURL/:id' 경로로 들어온 요청에 대해 id를 검증하고 redirectURL을 반환함 const url = redirectMap[req.params.id]; if (url) { res.json({ url }); } else { res.status(400).json({ error: 'Invalid redirect ID' }); } });

¹⁾ URL 리디렉션(URL redirection): 특정 URL을 열었을 때 다른 URL로 이동시키는 기술