"양쪽이 검증해. Recall 이 allowlist 된 DB 경로를 해석하고, Ashen Reel 은 열기 전에 돌려받은 canonical 파일이랑 루트를 자기가 또 검사해."
검사 둘, 하나가 아니라
딥링크가 도착해도 Ashen Reel 은 아직 아무것도 안 열어. 먼저 설정된 loopback Recall 서비스에 물어. 들어온 링크가 못 덮어쓰는 고정 로컬 base URL 이지. Recall 한테 video_id 를 진짜 경로로 바꿔달라고 하는 거야. Recall 이 자기 일을 해. id 를 데이터베이스에서 찾아서, 이미 allowlist 해둔 경로를 돌려주지. 여기서 끝날 수도 있어. 근데 안 끝나.
그다음 Ashen Reel 이 그 경로를 자기 경계 쪽에서 또 검증해. 돌려받은 파일을 canonicalize 하고, 진짜 regular file 인지 확인하고, symlink 를 다 푼 뒤에도 설정된 Recall 미디어 루트 안에 있는지 검사해. 그러고 나서야 열지. 두 독립 당사자가 각각 검사하는 거야. 어느 한쪽만으론 단일 실패점이니까.
symlink 탈출
'이 경로가 내 폴더 안이야?' 를 순진하게 하면 놓치는 미묘한 공격이 있어. 허용된 루트 안에 민감한 어딘가로 나가는 symlink 가 들어 있다고 해봐. literal 경로에 문자열 접두사 검사만 하면 "응, 허용 루트로 시작하네" 하고 통과시켜. 정작 링크를 따라가면 밖에 떨어지는데도. 고침은 순서에 있어. symlink 를 먼저 풀고, 그다음에 containment 를 검사해. 경로가 겉으로 뭐라 주장하는지가 아니라, 실제로 어디로 이어지는지를 검증하는 거야.
.. 세그먼트로도, symlink 로도, 인코딩 트릭으로도. 경로를 진짜의, 절대의, symlink 없는 형태로 먼저 풀고, 그걸 비교해. 비교는 그 앞의 canonicalization 만큼만 믿을 만하거든.독립 재검사는 짧아. 그리고 그 두 단계의 순서가 보안 전부야.
왜 앱이 base URL 을 소유하나
조용한 방어가 하나 더 있어. Ashen Reel 이 말 거는 loopback Recall 주소는 자기 설정 값이야. 들어온 링크의 필드가 절대 아니고. URL 이 "이 id 를 http://evil.example/api 에 대고 해석해" 라고 할 수 있으면, 공격자가 자기를 제약하려던 바로 그 조회의 답을 자기가 공급하는 꼴이야. 그래서 base URL 은 협상 대상이 아니야. 링크는 영상을 이름 붙이고, 어느 신뢰된 서비스가 그걸 해석할지는 앱 혼자 정해. 공격자가 끼어들 만한 모든 자리를, 설계가 미리 치워버린 거지.