/**
* YS Project - 추천 스타일 이미지 URL 안전성 판정 (공용)
*
* ⚠️ 이 파일은 서버측 `includes/style_helpers.php` 의
* `styleIsAllowedAssetPath()` / `styleSafeImageUrl()` 과 **같은 규칙**이어야 한다.
* 한쪽만 바꾸면 그 순간 규칙이 갈라지고, 갈라진 쪽이 실사용 경로가 되어 구멍이 된다.
*
* 왜 이 판정이 필요한가:
* 작품/배우 이미지 URL 은 **인증 없는 업체 Ingest API(`api/partner/*`)로 아무나 넣는다.**
* `cover_image_url` 에 `/api/logout.php` (또는 `https://<이 사이트>/api/logout.php`) 를 넣으면
* 목록을 여는 **모든 사용자·관리자가
로드만으로 강제 로그아웃**된다(실제 재현됨).
* 그래서 "이 사이트로 향하는" 이미지 URL 은 정적 자산 경로로만 제한한다.
* 외부 호스트 URL 은 계속 허용한다 — 외부 CDN 저장이 정상 동작이고,
* 다른 호스트로 가는 요청에는 이 사이트 세션 쿠키가 실리지 않는다.
*
* 이 파일은 전역 함수 하나만 노출한다. 사용하는 쪽은
* `admin/styles_works.php`, `admin/styles_actors.php`, `styles/index.php`.
*/
(function (global) {
'use strict';
/**
* 호스트 정규화 — 서버측 `styleSafeImageUrl()` 의 호스트 처리와 맞춘다.
*
* ⚠️ 딱 한 곳, **의도적으로 서버가 더 엄격하다.**
* 서버는 `intl` 확장이 없어 브라우저의 UTS46/IDNA 매핑을 재현할 수 없다. 그래서
* **비ASCII 호스트를 통째로 거부**한다(전각 `ys.com` 같은 위장을 막는 유일한 방법).
* 여기서는 `new URL().hostname` 이 이미 IDNA 정규화를 끝내주므로 그 검사가 필요 없고,
* 넣으면 오히려 정상적인 IDN 외부 CDN 을 과차단한다.
* → 결과 차이: 진짜 IDN 외부 호스트 이미지는 AJAX 목록엔 뜨고 SSR 엔 안 뜬다.
* 보안상 안전한 방향(서버가 더 조임)이고, intl 이 깔리면 서버쪽을 idn_to_ascii() 로 바꿔 해소할 것.
*/
function canonHost(host) {
var h = String(host || '').toLowerCase();
try { h = decodeURIComponent(h); } catch (e) { /* 깨진 인코딩은 원문 그대로 비교 */ }
return h.replace(/\.+$/, '');
}
/** 이미지로 허용할 내부 경로인지 (서버 styleIsAllowedAssetPath() 와 동일 규칙) */
function isAllowedAssetPath(path) {
if (!path || path.charAt(0) !== '/') return false;
var decoded;
try { decoded = decodeURIComponent(path); } catch (e) { return false; }
if (decoded.indexOf('\\') !== -1) return false;
// '..' 는 세그먼트 단위로만 거부한다 ('/uploads/my..old.jpg' 같은 정상 파일명은 통과)
var segs = decoded.split('/');
for (var i = 0; i < segs.length; i++) {
if (segs[i] === '..') return false;
}
// 접두사는 원문 기준 — '/%75ploads/' 같은 인코딩 위장을 함께 거른다
return path.indexOf('/uploads/') === 0 || path.indexOf('/assets/') === 0;
}
/**
* @param {string} url
* @returns {string} 안전하면 원본 URL, 아니면 '' (호출부는 '' 이면
를 만들지 않는다)
*/
function ysSafeImageUrl(url) {
if (!url) return '';
var v = String(url).trim();
if (v === '') return '';
if (/[\x00-\x1F\x7F]/.test(v)) return ''; // 제어문자로 스킴 검사 우회 차단
if (v.indexOf('//') === 0 || v.indexOf('/\\') === 0) return ''; // 프로토콜 상대 URL / 백슬래시 변형
if (v.charAt(0) === '/') {
return isAllowedAssetPath(v) ? v : '';
}
if (!/^https?:\/\//i.test(v)) return '';
var u;
try { u = new URL(v); } catch (e) { return ''; }
// 같은 호스트면 결국 같은 엔드포인트를 때린다 → 내부 경로와 동일 규칙 적용.
// u.pathname 은 브라우저가 이미 '..' 와 '\' 를 정규화한 결과라 서버측 검사와 결론이 같아진다.
// 호스트는 서버(styleSafeImageUrl)와 **똑같이** 트레일링 닷·퍼센트 인코딩을 풀고 비교한다 —
// 안 맞추면 `ys.com.` / `ys.com%2e` 가 서버에선 차단, 여기선 통과로 갈려 규칙이 발산한다.
if (canonHost(u.hostname) === canonHost(global.location.hostname)) {
return isAllowedAssetPath(u.pathname) ? v : '';
}
return v; // 외부 호스트 허용
}
global.ysSafeImageUrl = ysSafeImageUrl;
})(window);