Android WebView 안전하게 설정하는 방법 (JS 허용, 외부 URL 분기, 파일 접근 차단)
하이브리드 앱을 개발하거나 운영하다 보면 앱 내부에 웹 화면을 넣기 위해 WebView를 정말 자주 쓰게 됩니다. 저의 경우도 공지사항, 이벤트 페이지, 또는 결제 화면 등을 WebView로 처리하곤 하는데요.
웹 페이지가 정상적으로 동작해야 하니까 처음 개발할 때 별생각 없이 JavaScript를 켜고 관련 설정을 느슨하게 다 열어두는 경우가 많습니다.
하지만 이렇게 설정을 열어두면 XSS 같은 웹 보안 취약점이 그대로 앱의 취약점으로 이어집니다. 특히 내부 도메인과 외부 링크 구분이 안 되거나, file:// 접근이 허용된 상태에서 JavaScript가 켜지면 로컬에 있는 민감한 데이터가 유출될 위험도 있습니다. 매번 프로젝트 세팅할 때마다 설정 값이 헷갈려서, 실제 운영 환경에 맞춰 안전하게 쓸 수 있는 가이드와 공통 코드를 정리해 둡니다.
1. 안전한 WebView 공통 설정 코드
매번 화면마다 설정을 따로 복사해서 넣으면 누락되는 부분이 생기기 쉽습니다. 아래처럼 공통으로 검증하고 세팅할 수 있는 Configurator 클래스를 만들어 두고 호출하는 방식이 안전합니다.
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:// 기반 취약점 공격 표면이 되므로 대안을 찾아야 합니다. |
| 외부 URL | Allowlist 기반 처리 | 앱 안에서 처리할 내부 도메인 | 내부 도메인 외에는 다 외부 브라우저로 튕겨내야 안전합니다. |
| JS Interface | 최소화 및 검증 | 웹에서 네이티브 기능을 부를 때 | 노출할 메서드에 @JavascriptInterface를 붙이고 파라미터 검증 필수. |
1) JavaScript Interface 노출 최소화
addJavascriptInterface()는 웹에서 네이티브 앱 기능을 마음대로 호출할 수 있는 강력한 기능입니다. 공식 문서에서도 경고하듯, 만약 악성 스크립트가 주입되면 앱 전체 제어권이 넘어갈 수 있습니다. 진짜 필요한 기능만 최소한으로 노출하고, 전달받는 인자값(String 등)이 올바른 형식인지 반드시 방어 코드를 짜야 합니다.
2) file:// 로컬 접근이 위험한 이유
allowFileAccessFromFileURLs나 allowUniversalAccessFromFileURLs를 켜두면, 로컬 파일 권한을 가진 스크립트가 앱 내부의 고유 데이터(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)을 당하거나 보안 취약점 리포트를 받으면 코드를 다 갈아엎어야 해서 일이 더 커집니다. 처음부터 "기본은 다 막고, 필요한 도메인과 기능만 허용한다"는 원칙으로 공통 클래스를 만들어 두고 관리하시는 것을 추천합니다.
댓글
댓글 쓰기