From e537a3485466645a25149b5561747e814471af09 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 22 Sep 2026 16:43:52 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=99=B8=EB=B6=80=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EC=97=90=20session=5Fid=EB=A5=BC=20=EB=B6=99=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EA=B3=A0=20Android=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20=EC=97=B4=EB=A6=AC=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 두 증상이 한 경로에서 나온다. 홈 웹뷰에서 외부 링크를 탭하면 handleShouldStartLoadWithRequest 가 가로채 /webview/[slug] 로 넘기는데, 그 화면이 목적지를 가리지 않고 appendSessionId 를 붙였다. iOS: session_id 는 웹 Mixpanel 의 distinct_id 다. 그게 쿼리에 실려 제3자 도메인으로 나가 상대 액세스 로그에 남는다. 모아동 오리진일 때만 붙인다. Android: setSupportMultipleWindows 기본값이 true 라 target=_blank 가 onCreateWindow 로 간다. onOpenWindow 핸들러가 없으면 RNCWebChromeClient 가 WebViewClient 도 없는 new WebView(context) 를 만들어 transport 로 넘기는데, 그 뷰는 어떤 계층에도 붙지 않는다. 그래서 링크를 눌러도 아무 일이 안 일어난다. false 로 두면 같은 요청이 onShouldStartLoadWithRequest 를 타 iOS 와 같은 경로가 된다. 둘을 같이 고친다. Android 링크만 살리면 session_id 가 나가는 경로가 Android 로도 번진다. 대상은 preventDefault 없는 생 들이다(ClubUnionPage 의 인스타·카톡, IntroducePage 의 문의하기). useNavigator 를 거치는 링크는 requestOpenExternalUrl -> WebBrowser 로 나가므로 원래 영향이 없다. Co-Authored-By: Claude Opus 5 (1M context) --- app/webview/[slug].tsx | 9 ++++++++- ui/home/home-webview-screen.tsx | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/webview/[slug].tsx b/app/webview/[slug].tsx index a5c4e70..d7ad962 100644 --- a/app/webview/[slug].tsx +++ b/app/webview/[slug].tsx @@ -76,7 +76,14 @@ export default function WebViewScreen() { ? (config.url ?? (config.path ? `${webviewUrl}${config.path}` : "")) : ""; - const url = useMemo(() => appendSessionId(baseUrl, sessionId), [baseUrl, sessionId]); + /** + * session_id 는 웹 Mixpanel 의 distinct_id 라 모아동 밖으로 나가면 안 된다. + * 이 화면은 slug=external 로 임의 외부 URL 도 로드하므로 오리진을 확인하고 붙인다. + */ + const url = useMemo( + () => (isWebViewOrigin(baseUrl) ? appendSessionId(baseUrl, sessionId) : baseUrl), + [baseUrl, sessionId], + ); /** * 우체통은 앱이 주입한 학생 토큰을 먼저 쓴다(웹 studentFetch). 주입이 없으면 웹이 diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index 5c79613..668f211 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -230,6 +230,10 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { onHttpError={handleError} javaScriptEnabled domStorageEnabled + // Android 기본값(true)이면 target=_blank 가 onCreateWindow 로 가는데, + // onOpenWindow 핸들러가 없으면 화면에 붙지 않는 WebView 로 빨려들어가 링크가 죽는다. + // false 로 두면 같은 요청이 onShouldStartLoadWithRequest 를 타 iOS 와 같은 경로가 된다. + setSupportMultipleWindows={false} pullToRefreshEnabled allowsBackForwardNavigationGestures /> From 00423979c4e6a85701a0eac2bcbbe18c89de3898 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Tue, 22 Sep 2026 16:56:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=9B=B9=EB=B7=B0=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20URL=20=ED=8C=90=EC=A0=95=EC=9D=84=20prefix=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20origin=20=EB=B9=84=EA=B5=90=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit request.url.startsWith(baseOrigin) 은 https://moadong.com.evil.com 을 내부 URL 로 승인한다. 그 페이지가 홈 웹뷰에 뜨면 window.ReactNativeWebView.postMessage 로 브리지를 그대로 쓸 수 있다. 이 웹뷰는 onMessage 에서 SUBSCRIBE_TOGGLE, NAVIGATE_WEBVIEW, OPEN_EXTERNAL_URL, SHARE 를 origin 검증 없이 처리한다. 파싱한 origin 끼리 비교한다. #40 에서 추가한 isWebViewOrigin 을 그대로 쓴다. 주입 토큰 쪽은 원래 new URL(...).origin 으로 비교하고 있어 영향이 없었다. setSupportMultipleWindows={false} 로 Android 의 target=_blank 요청도 이 검사에 들어오므로 같이 고친다. CodeRabbit 지적 반영. Co-Authored-By: Claude Opus 5 (1M context) --- ui/home/home-webview-screen.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/home/home-webview-screen.tsx b/ui/home/home-webview-screen.tsx index 668f211..e76751c 100644 --- a/ui/home/home-webview-screen.tsx +++ b/ui/home/home-webview-screen.tsx @@ -2,7 +2,7 @@ import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-co import { useMixpanelContext } from '@/contexts/mixpanel-context'; import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context'; import { ensureAccessToken } from '@/services/auth-token.service'; -import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent } from '@/utils/webview'; +import { appendSessionId, buildStudentTokenInjection, getWebViewUserAgent, isWebViewOrigin } from '@/utils/webview'; import Constants from 'expo-constants'; import { useRouter } from 'expo-router'; import * as WebBrowser from 'expo-web-browser'; @@ -174,8 +174,10 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) { const handleShouldStartLoadWithRequest = useCallback( (request: ShouldStartLoadRequest) => { - const baseOrigin = (process.env.EXPO_PUBLIC_WEBVIEW_URL ?? 'https://moadong.com').replace(/\/$/, ''); - if (request.url.startsWith('http') && !request.url.startsWith(baseOrigin)) { + // origin 을 문자열 prefix 로 판정하면 moadong.com.evil.com 이 내부로 통과한다. + // 그 페이지가 이 웹뷰에 뜨면 window.ReactNativeWebView.postMessage 로 브리지를 + // 그대로 쓸 수 있다(SUBSCRIBE_TOGGLE, OPEN_EXTERNAL_URL 등). 파싱해서 비교한다. + if (request.url.startsWith('http') && !isWebViewOrigin(request.url)) { // iOS: navigationType === 'click' 은 사용자가 직접 링크를 탭한 경우만 해당 // 초기 로드·서버 리다이렉트는 'other' 이므로 인터셉트하지 않음 // Android: navigationType이 항상 'other'이므로 loaded 상태로 구분