Android WebView 안전하게 설정하는 방법 (JS 허용, 외부 URL 분기, 파일 접근 차단)

하이브리드 앱을 개발하거나 운영하다 보면 앱 내부에 웹 화면을 넣기 위해 WebView를 정말 자주 쓰게 됩니다. 저의 경우도 공지사항, 이벤트 페이지, 또는 결제 화면 등을 WebView로 처리하곤 하는데요.

웹 페이지가 정상적으로 동작해야 하니까 처음 개발할 때 별생각 없이 JavaScript를 켜고 관련 설정을 느슨하게 다 열어두는 경우가 많습니다.

하지만 이렇게 설정을 열어두면 XSS 같은 웹 보안 취약점이 그대로 앱의 취약점으로 이어집니다. 특히 내부 도메인과 외부 링크 구분이 안 되거나, file:// 접근이 허용된 상태에서 JavaScript가 켜지면 로컬에 있는 민감한 데이터가 유출될 위험도 있습니다. 매번 프로젝트 세팅할 때마다 설정 값이 헷갈려서, 실제 운영 환경에 맞춰 안전하게 쓸 수 있는 가이드와 공통 코드를 정리해 둡니다.

1. 안전한 WebView 공통 설정 코드

매번 화면마다 설정을 따로 복사해서 넣으면 누락되는 부분이 생기기 쉽습니다. 아래처럼 공통으로 검증하고 세팅할 수 있는 Configurator 클래스를 만들어 두고 호출하는 방식이 안전합니다.

Kotlin
class SecureWebViewConfigurator(
    private val allowedHosts: Set<String>,
) {
    fun configure(webView: WebView) {
        webView.settings.apply {
            // 웹 서비스 동작을 위해 필요한 경우만 켭니다.
            javaScriptEnabled = true
            domStorageEnabled = true

            // 로컬 파일 접근은 기본적으로 모두 차단합니다.
            allowFileAccess = false
            allowContentAccess = false
            allowFileAccessFromFileURLs = false
            allowUniversalAccessFromFileURLs = false

            // HTTPS 환경에서 HTTP 리소스가 로드되지 않도록 설정
            mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
        }

        // 배포(Release) 빌드에서는 웹뷰 디버깅을 반드시 꺼야 합니다.
        WebView.setWebContentsDebuggingEnabled(false)

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView,
                request: WebResourceRequest,
            ): Boolean {
                val uri = request.url
                return handleUrl(view, uri)
            }
        }
    }

    private fun handleUrl(webView: WebView, uri: Uri): Boolean {
        val scheme = uri.scheme ?: return true
        val host = uri.host ?: return true

        // 1. 허용된 내부 HTTPS 도메인만 웹뷰 안에서 엽니다.
        if ((scheme == "https") && allowedHosts.contains(host)) {
            return false // WebView 내부에서 계속 로딩
        }

        // 2. 운영 환경에서는 보안상 HTTP URL은 무조건 차단합니다.
        if (scheme == "http") {
            return true // 로딩 차단
        }

        // 3. 외부 HTTPS 링크는 기본 브라우저로 넘깁니다.
        if (scheme == "https") {
            openExternal(webView.context, uri)
            return true
        }

        // 4. 전화, 메일, 마켓 링크 등은 네이티브 외부 Intent로 처리합니다.
        if (scheme in setOf("tel", "mailto", "market")) {
            openExternal(webView.context, uri)
            return true
        }

        return true // 알 수 없는 스킴은 기본 차단
    }

    private fun openExternal(context: Context, uri: Uri) {
        val intent = Intent(Intent.ACTION_VIEW, uri)
        if (intent.resolveActivity(context.packageManager) != null) {
            context.startActivity(intent)
        } else {
            Toast.makeText(context, "링크를 열 수 없습니다.", Toast.LENGTH_SHORT).show()
        }
    }
}

2. 핵심 설정 기준과 주의할 점

설정을 기본값으로 그냥 두거나 무조건 켜기 전에, 아래 가이드라인을 기준으로 체크해 보셔야 합니다.

항목기본 방향켜야 하는 경우주의할 점
JavaScript기본 비활성웹 화면이 JS 없이는 안 돌 때신뢰할 수 있는 도메인만 안에서 열리게 제한해야 합니다.
DOM Storage필요할 때만 활성화웹 로그인 세션 유지가 필요할 때로컬 저장소에 암호화되지 않은 민감 정보가 남는지 확인 필요.
File Access무조건 비활성 권장로컬 HTML/파일을 꼭 보여줘야 할 때file:// 기반 취약점 공격 표면이 되므로 대안을 찾아야 합니다.
외부 URLAllowlist 기반 처리앱 안에서 처리할 내부 도메인내부 도메인 외에는 다 외부 브라우저로 튕겨내야 안전합니다.
JS Interface최소화 및 검증웹에서 네이티브 기능을 부를 때노출할 메서드에 @JavascriptInterface를 붙이고 파라미터 검증 필수.

1) JavaScript Interface 노출 최소화

addJavascriptInterface()는 웹에서 네이티브 앱 기능을 마음대로 호출할 수 있는 강력한 기능입니다. 공식 문서에서도 경고하듯, 만약 악성 스크립트가 주입되면 앱 전체 제어권이 넘어갈 수 있습니다. 진짜 필요한 기능만 최소한으로 노출하고, 전달받는 인자값(String 등)이 올바른 형식인지 반드시 방어 코드를 짜야 합니다.

2) file:// 로컬 접근이 위험한 이유

allowFileAccessFromFileURLsallowUniversalAccessFromFileURLs를 켜두면, 로컬 파일 권한을 가진 스크립트가 앱 내부의 고유 데이터(SharedPreferences나 DB 파일 등)를 읽어서 외부로 탈취할 수 있는 구조가 만들어집니다.

로컬 asset 폴더의 HTML을 보여줘야 하는 상황이라면 file:// 경로를 직접 열지 말고, Google에서 권장하는 WebViewAssetLoader를 사용하는 편이 훨씬 안전합니다.

3) URL Allowlist 검증 시 흔히 하는 실수

도메인을 검증할 때 uri.host?.contains("my-service.com") 같은 방식으로 짜는 경우가 많습니다. 이렇게 하면 evil-my-service.com 처럼 유사하게 도메인을 판 뒤 접근해도 필터가 뚫려버립니다. 문자열 포함 여부(contains)가 아니라, 위에 작성한 코드처럼 완전히 일치하는 Host 목록(Set<String>)을 만들어서 비교하거나 정교한 도메인 파싱 함수를 쓰셔야 합니다.

3. 배포 전 보안/운영 체크리스트

  • [ ] JavaScript는 꼭 필요한 화면에서만 활성화했는가?

  • [ ] WebView 내부에서 로드할 도메인을 허용 목록(Allowlist)으로 통제하고 있는가?

  • [ ] HTTP URL이나 알 수 없는 커스텀 스킴은 기본 차단(True 반환)했는가?

  • [ ] allowFileAccessFromFileURLs, allowUniversalAccessFromFileURLs를 명시적으로 false 처리했는가?

  • [ ] 배포(Release) 빌드 시 setWebContentsDebuggingEnabled(false)가 확실히 적용되는가?

  • [ ] (중요) 블로그나 GitHub 예제 코드, 로그 출력부에 실제 운영 서버 URL, API 키, 테스트용 계정 정보가 남아있지 않은가?

4. 요약 및 요령

운영하다 보면 "왜 웹뷰에서 링크가 안 열리냐", "결제 창이 하얗게 나온다" 같은 문의를 받고 그제야 설정을 하나씩 열어주는 경우가 많습니다.

처음부터 다 열어두고 개발하면 편하긴 하겠지만, 나중에 구글 플레이 스토어 출시 거절(Rejection)을 당하거나 보안 취약점 리포트를 받으면 코드를 다 갈아엎어야 해서 일이 더 커집니다. 처음부터 "기본은 다 막고, 필요한 도메인과 기능만 허용한다"는 원칙으로 공통 클래스를 만들어 두고 관리하시는 것을 추천합니다.

댓글

이 블로그의 인기 게시물

안드로이드 화면 꺼짐 방지 FLAG_KEEP_SCREEN_ON 및 WakeLock 적용 방법

안드로이드 백그라운드 타이머 구현 및 Foreground Service 알림 설계 정리

관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)