Cheondi
개발 · 웹

WebView·네이티브 외부 링크 처리

웹 링크를 앱 내부 WebView와 외부 브라우저 중 어디서 열지 안전한 정책으로 나눈 기록입니다.

  • #webview
  • #deeplink
  • #mobile

공지 링크를 웹에서는 새 탭으로, 모바일 앱에서는 WebView로 열도록 붙였다. 그런데 로그인이나 파일 다운로드 링크까지 WebView 안에 들어가 돌아오지 못하거나, 외부 앱 스킴을 일반 URL처럼 열어 오류가 났다. 링크 모양만 보고 실행 환경별 API를 호출하면 충분하지 않았다.

링크의 목적과 신뢰 수준에 따라 열기 정책을 정해야 했다.

URL 분류 이후의 실행 경로

파싱한 URL의 프로토콜과 호스트, 경로를 기준으로 허용된 동작을 선택했다.

LinkAction Classify(Uri uri)
{
    if (uri.Scheme == "https" && trustedHosts.Contains(uri.Host))
        return LinkAction.InAppWebView;
    if (uri.Scheme == "https")
        return LinkAction.ExternalBrowser;
    if (allowedDeepLinks.Contains(uri.Scheme))
        return LinkAction.ConfirmAndOpenExternal;
    return LinkAction.Block;
}

문자열 prefix 비교 대신 URL 파서를 사용해 비슷하게 보이는 악성 호스트를 구분했다.

WebView 최소 권한 노출

신뢰한 페이지라도 JavaScript 브리지가 모든 네이티브 기능을 노출하지 않게 허용 메시지와 origin을 확인했다. 새 창 요청, 파일 선택, 위치 권한은 기본 거부하고 필요한 기능만 제품 정책에 따라 열었다.

WebView에서 외부 호스트로 이동하려 하면 중단하고 외부 브라우저로 전환할지 사용자에게 안내했다.

외부 이동 후 복귀·실패 흐름

외부 브라우저나 다른 앱으로 이동한 뒤 돌아오면 앱이 이전 화면 상태를 유지해야 했다. 딥링크 결과가 필요한 흐름은 state를 검증하고 이미 처리한 콜백을 다시 실행하지 않았다.

대상 앱이 설치되지 않았거나 URL을 열 수 없으면 조용히 실패하지 않고 복사 가능한 안전한 웹 대안을 제공했다.

플랫폼 통합 기능으로서의 링크 처리

처음에는 OpenURL 한 줄로 끝날 작업처럼 보였다. 실제로는 웹 콘텐츠, 네이티브 권한, 외부 앱 사이를 넘는 경계였다. 어디에서 열리는지가 보안과 로그인 상태, 복귀 경험을 바꿨다.

이후 링크 기능을 만들 때 URL 검증, 실행 위치, 권한, 실패 대안, 앱 복귀를 한 세트로 본다. 같은 링크도 맥락에 따라 안전한 열기 방법이 달랐다.

후속 확인 항목

작업 당시에는 눈앞의 오류를 먼저 고쳤지만, 이후 기술 문서와 맞춰 보며 확인 순서를 조금 더 구체화했다.

Android의 WebView bridge 보안 문서는 addJavascriptInterface로 노출한 객체가 모든 frame에서 접근될 수 있고 호출한 frame의 origin을 애플리케이션이 검증하기 어렵다는 점을 경고한다. 외부 링크 처리와 네이티브 호출을 같은 bridge에 넣으면 신뢰 경계가 지나치게 넓어진다.

링크는 scheme·host를 허용 목록으로 확인하고, 네이티브 기능은 필요한 메서드만 좁게 노출하는 편이 낫다.

다음에 WebView·네이티브 외부 링크 처리 같은 문제를 보면 아래 순서부터 확인하려고 한다.

  • 현재 URL과 history entry 수
  • 허용 scheme·host·최종 redirect
  • 앱 복귀 시 화면 수명주기
  • 외부 이동 실패의 fallback

신뢰하지 않는 페이지를 열기 전 bridge를 제거하고 HTTPS·redirect 최종 주소까지 확인해야 처음 URL만 검사하는 빈틈을 줄일 수 있다.

공식 참고 자료