서버측 본문 정제와 게시 화면 스타일 (v10에 없는 것)
이 문서는 2026-09-28에 새로 썼습니다.
이 문서의 존재 이유가 "없는 것을 알려 주는 것"입니다.
v10을 쓰는 분이 v11 문서를 읽으면 존재하지 않는 기능을 안내받습니다. 그 구분을 실측으로 확정하기 위해 썼습니다. 여기 적힌 "없다"는 모르는 게 아니라 확인한 결과입니다.
0. 먼저 결론 3줄
- v10.5.1에는 서버측 본문 정제 진입점이 없습니다.
config/t2_sanitize.php는 404,T2Esanhtml같은 심볼은 173개 php/js/inc 파일에 0건입니다. - 그누보드5·라이믹스 위에서는 CMS 자체 필터가 방어선입니다. 단 T2Editor가 그 필터를 부르는 코드는 v10에 없습니다 — CMS가 자기 경로에서 도는 것이지, T2Editor 본문 저장 지점에서 거르는 것이 아닙니다.
- v10의 게시 화면 스타일 해법은 수동
<link>입니다. 본문 끝에 자동 삽입되는css/content.css링크가 CMS 필터에게 지워질 수 있기 때문입니다. v11의?bundle=content·format=js로더는 v10에 없습니다.
1. 서버측 본문 정제 — v10에 없다
1-1 · 실측 (추측 아님)
releases/v10.5.1 recursive tree = blob 290 / tree 59, truncated=false. 그중 php 86개 + js 86개 + inc 1개 = 173개 파일을 100% 로컬로 받아 심볼을 전수 검색했다.
검색 범위 명시: 290개 중 나머지 117개는json76 ·css19 ·txt5 ·htaccess3 ·html3 ·woff22 ·wasm3 ·bin2 ·xml1 · 확장자 없음 3 이며 심볼 검색에서 제외했습니다..inc1건(extend/php/t2_first_run_setup_full.inc)은 포함했고 10개 심볼 전부 0건입니다. 제외한 파일 종류에 PHP 심볼이 있을 수 없다는 것은 근거가 아니라 가정입니다 — §10-3 참조.
| 확인 대상 | 결과 |
|---|---|
config/t2_sanitize.php | HTTP 404 (파일이 없음) |
T2Esanhtml | 0 파일 |
T2Esanclean | 0 파일 |
T2Esanregisterprovider | 0 파일 |
t2_sanitize (문자열 전체) | 0 파일 |
config/t2_hard_config.php | HTTP 404 |
config/t2_cms_security.php | HTTP 404 |
docs/ 디렉터리 | 없음 (S2 guide.txt §10이 안내하는 docs/content-sanitizer.md 등 전부 v10에 없음) |
1-2 · getSanitizedContent() 이름은 오해를 부릅니다
이 이름 때문에 "브라우저에서라도 거르겠구나"로 읽기 쉽습니다. 37줄을 전부 읽었습니다 (js/core.js:2783-2819). 하는 일은 이 4가지뿐입니다.
js// js/core.js:2789-2795 tmp.querySelectorAll('p[data-t2-boundary], p[data-t2-generated]').forEach((p) => { p.removeAttribute('data-t2-boundary'); p.removeAttribute('data-t2-generated'); p.removeAttribute('contenteditable'); }); tmp.querySelectorAll(':scope > p[contenteditable]').forEach((p) => p.removeAttribute('contenteditable')); tmp.querySelectorAll('br[data-t2-placeholder]').forEach((br) => br.removeAttribute('data-t2-placeholder')); // js/core.js:2804-2811 에디터 placeholder로 쓰는 ZWSP(U+200B)/BOM(U+FEFF)만 제거 // js/core.js:2814-2818 속성 없는 빈 <p></p>를 <p><br></p>로 치환
| 하는 일 | 하지 않는 일 |
|---|---|
| 에디터 내부 전용 경계 표식 제거 | <script> 제거 |
contenteditable 속성 제거 | onerror·onclick 같은 이벤트 속성 제거 |
| placeholder용 ZWSP/BOM 제거 | javascript: URL 차단 |
| 빈 문단 정규화 | <iframe>·<object> 제거 |
주석에도 이유가 적혀 있습니다: "빈 줄 자체는 의미가 있으므로 <p><br></p> 형태로 보존한다", "ZWNJ(U+200C)·ZWJ(U+200D)는 페르샤어 형태/복합 이모지 등에 실제 의미가 있으므로 사용자 본문에서 제거하면 안 된다." ⇒ 본문 내용을 정리하는 함수이지, 보안을 하는 함수가 아닙니다.
1-3 · 제출(저장) 경로에서 제거하는 것은 클래스 1개뿐
editor.core.php:1768-1838 이 폼 제출 직전 본문을 정리하는 전부입니다. remove() 는 3곳 나오는데 2곳은 표 래퍼 DOM 이동이고, 본문에서 제거하는 것은 하나:
php// editor.core.php:1798 tempDiv.querySelectorAll('.t2-move-controls').forEach(function (ctrl) { ctrl.remove(); }); // :1837 document.getElementById({$js_id}).value = finalContent;
.t2-move-controls 는 에디터 UI 손잡이입니다. 사용자 본문에서 하나도 제거되지 않습니다.
1-4 · 그럼 방어는 전무합니까 — 아닙니다
비대칭이 정확합니다. v10에도 확실히 있는 방어들입니다.
| 방어 | 근거 (S3 실측) |
|---|---|
| iframe 허용 호스트 목록 | config/t2_config.php:304-305 T2EDITOR_ALLOWED_IFRAME_DOMAINS + :328-338 t2editor_get_allowed_iframe_domains() · 브라우저 검증 js/core.js:4659-4660 · 관리자 저장값 승격 extend/php/t2admin_settings_loader.php:52-57 |
| 같은 출처(Origin) 검사 | config/upload_config.php:229-255 check_request_origin() — 변경 요청에 Origin 이 없으면 거부, 역방향 프록시 뒤의 X-Forwarded-Proto 를 반영. 호출처 10곳 (업로드·프록시·iframe·최초설치 API 등) |
| 업로드 파일 시그니처 검사 | config/upload_config.php:28-33 주석: "server-side identification uses signatures so Fileinfo is not required" |
| 업로드 확장자 허용목록 (SVG·ICO 제외) | config/upload_config.php:54-75 |
| 파일명 정제 | config/upload_config.php:258 sanitize_original_name() — NUL·경로 구분자·제어 문자 제거, 길이 제한 |
| NSFW 이미지 필터 | vendor/nsfwjs/ 번들 + config/nsfw_api_browser.{js,php} (S1 10.4.0: 외부 서버 의존 제거) |
⇒ 정확한 서술은 "v10에 보안 기능이 없다"가 아니라 「URL·업로드·Origin 축은 방어가 있고, 본문 HTML 전체를 걷어내는 축만 비어 있다」 입니다.
2. 그누보드5·라이믹스 위에서는 누가 거릅니다
2-1 · ⚠ 여기서 과장하지 않기
S2 guide.txt §3.4 와 §4.4 는 v11이 그 CMS 필터에 연결했다는 기록입니다 — 각 행 끝에 ← 11.0 에서 연결 이라고 붙어 있습니다. 그래서 v10에 대해 정확한 서술은 이렇습니다.
| 서술 | 판정 |
|---|---|
"그누보드5는 clean_xss_tags() 를 제공한다" | ✅ — CMS가 제공하는 함수입니다 (T2Editor와 무관하게 그누보드5에 존재) |
"라이믹스는 HTMLFilter::clean() (HTMLPurifier) 을 제공한다" | ✅ — CMS가 제공하는 클래스입니다 |
| "v10 T2Editor가 그 필터를 본문 저장 지점에서 부른다" | ❌ 틀렸습니다. clean_xss_tags 0건, HTMLFilter 0건, HTMLPurifier 는 plugin/collab/collab_number.core.php:118 주석 1건뿐입니다 |
⇒ 정확한 문장: "그누보드5와 라이믹스는 CMS 자신의 저장·출력 경로에서 HTML 필터를 돌립니다. T2Editor 본문은 그 필터를 통과합니다. 그것은 CMS가 하는 일이지, v10 T2Editor가 하는 일이 아닙니다. v10 배포판에는 이 필터를 T2Editor 저장 지점에서 부르는 코드가 없습니다."
get_safe_filename·check_request_origin(CMS판) ·get_token·spamfilter_denied_word·Session::createToken·FilenameFilter·FileContentFilter도 v10 173파일 기준 0건입니다.MediaFilter만integration/cms/rhymix/iframe_autoregister.php:17,582곳 — 그것도 본문 정화가 아니라 동영상 iframe 등록용입니다.
2-2 · 배치별 실질 방어선
| 배치 | 본문 정제 | 실질 방어선 | 사용자가 할 수 있는 것 |
|---|---|---|---|
| 그누보드5 | T2Editor에 없음 | CMS의 clean_xss_tags() (CMS 경로) + T2Editor의 iframe allowlist | CMS 필터를 신뢰하되, CMS 필터가 걷지 않는 속성을 알 수 없음을 인정 |
| 라이믹스 | T2Editor에 없음 | CMS의 HTMLFilter::clean() (HTMLPurifier) | 위와 같음 |
| 스탠드얼론 | T2Editor에 없음 | CMS도 없음 ⇒ 이 축이 완전히 비어 있음 | 직접 정제를 붙여야 함 (§2-3) |
| 워드프레스 | T2Editor에 없음 | CMS 필터 사용 가능하나 어댑터 미사용 | CMS 필터 + 별도 확인 |
2-3 · 스탠드얼론에서 정제를 붙일 수 있는 지점
"붙일 지점이 배포판에 없다"는 게 정확합니다. v10 배포판에 정제 모듈이 없으니까요. 그러나 저장 지점은 있습니다 — CMS가 없으면 그 지점은 사용자 자신의 저장 코드입니다.
php// ① 라이브러리를 먼저 넣습니다 (v10 배포판에 없으므로 직접 추가) require_once __DIR__ . '/sanitize.php'; // 예: HTML Purifier // ② 저장 직전에 한 번 거릅니다 $body = $_POST['content'] ?? ''; $purifier = new HTMLPurifier(array( 'HTML.Allowed' => 'p,br,strong,em,ul,ol,li,a[href|title],img[src|alt],table,thead,tbody,tr,td,th,pre,code,blockquote', 'URI.AllowedSchemes' => array('http' => true, 'https' => true, 'mailto' => true, 'data' => true), )); $safe = $purifier->purify($body); save_article($safe);
필수 요건 3가지:
- 허용목록 방식으로 하십시오. 차단목록(
<script>지우기) 방식은 새 벡터를 못 막습니다. - URL 스킴을 막으십시오.
javascript:,vbscript:,data:text/html,blob:,file:은href·src어디에서도 통과시키지 않습니다. <iframe>을 쓴다면 호스트를 허용목록으로 제한하십시오. v10 T2Editor의 iframe allowlist와 같은 목록을 쓰는 것이 가장 일관됩니다(config/t2_config.php:304-340).
⚠ 경고 하나: 배포판을 갱신할 때 이 코드는 덮어써지지 않습니다 —sanitize.php는 배포판에 없기 때문입니다. 다만config/아래에 두면 유지보수 대상이 헷갈리니, CMS 바깥(예:includes/)에 두십시오.
2-4 · 라이선스가 이 공백을 사용자에게 넘긴다는 점
readme.txt:42 — T2 서비스는 "있는 그대로(AS-IS)" 제공되며 어떠한 책임도 지지 않습니다. readme.txt:51 — 코드와 호환 구조는 사전 공지 없이 변경될 수 있습니다.
⇒ "다음 판본에 필터가 들어오겠다"는 기대가 계약으로 보장되지 않습니다. 위 §2-3 처럼 직접 붙이시는 편이 안전합니다.
3. ⭐ 게시 화면 스타일 — v10의 정답은 수동 링크입니다
3-1 · v10은 본문 끝에 링크를 자동으로 붙입니다
php// editor.core.php:1830-1836 var contentStyle = '<link href="' + {$js_editor_url} + '/css/content.css" rel="stylesheet">'; var finalContent = tempDiv.innerHTML; // Append content.css only when absent so repeated edit/publish cycles do not accumulate duplicate links. if ((finalContent.indexOf('t2-media-block') !== -1 || finalContent.indexOf('t2-table') !== -1 || finalContent.indexOf('t2-code-block') !== -1 || finalContent.indexOf('t2-drawing-block') !== -1) && finalContent.indexOf('/css/content.css') === -1) { finalContent += contentStyle; }
읽으면 알 수 있는 세 가지:
- 본문 끝(
+=)에 붙습니다.<head>가 아닙니다. - 해당 블록이 있을 때만 붙습니다(미디어·표·코드·그림 블록). 순수 텍스트 글에는 안 붙습니다.
- 이미 있으면 안 붙입니다 (반복 저장 시 누적 방지). ⇒ CMS가 링크를 지워버리면 다음 저장부터 매번 다시 붙습니다.
3-2 · v10의 CMS 어댑터는 head 에 아무것도 넣지 않습니다
head_script · head_tag 같은 주입 지점을 173개 파일(php 86 + js 86 + inc 1) 전수 검색했습니다.
| 검색어 | 결과 |
|---|---|
head_script | 0건 |
</head> | 6건 — 전부 독립 문서(관리자 페이지·동영상 플레이어·PDF 뷰어·오디오 플레이어·내보내기) |
content.css | 4건 — 3건이 위 editor.core.php:1830-1836, 1건이 plugin/image/hooks.js:10 주석 |
⇒ 그누보드5 어댑터도 라이믹스 어댑터도 게시 페이지 <head> 에 링크를 넣지 않습니다. 10.5.1 코드까지 확인했습니다.
3-3 · 그래서 수동 링크가 v10의 정답입니다
CMS가 t2editor 저장 지점을 지나는 경로에는 보통 HTML 필터가 붙습니다. 그 필터는 남의 글에 외부 스타일시트를 심지 못하게 막습니다 — 그것이 정상 동작입니다. 그래서:
- v10: 본문 끝의
<link>가 CMS 필터에게 지워질 수 있음 → 스킨/레이아웃에 직접<link>를 넣는 것이 v10의 정답입니다. (N-03-migration-v9-to-v10.md 및 기존 위키의content.css수동 링크 안내가 유효한 이유입니다. "낡은 지시"가 아닙니다.) - v11:
scope·format=js로더·T2Econtentstyleattach()로 이 문제를 구조적으로 해결. v10에는 없습니다.
⚠ 정확히 무슨 일이 벌어지느냐는 CMS·스킨·필터 설정에 따라 다르고, v10 배포판 안에서는 그 필터를 직접 볼 수 없습니다. 그래서 "무조건 지워진다"고 단정하지 않습니다. 확인 없는 말로 바꾸면: "지워질 수 있습니다. 그래서 v10에서는 수동 링크를 안전 장치로 두십시오."
3-4 · v10의 게시 화면이 밋밋할 때 확인할 것
| 증상 | 먼저 볼 것 |
|---|---|
| 표 셀이 붙어 보인다 | css/content.css 링크가 살아 있는지 |
| 이미지·영상 블록 레이아웃이 깨진다 | 동일 |
| 아무 변화가 없다 | 링크가 <head> 가 아니라 본문 끝에 있다 — 브라우저는 이를 허용하지만 CMS 필터가 지울 수 있음 |
| 링크가 매번 새로 붙는다 | 3-1 의 3번 — 지워졌다는 뜻입니다 |
4. ⭐ v10 과 v11 차이표 — v10 사용자에게 v11 안내를 하지 않기 위한 표
4-1 · v10에 없는 것 (전부 실측)
| v11 계약 (S2 guide.txt) | v10.5.1 실측 | v10 사용자가 겪는 일 |
|---|---|---|
§6 서버측 XSS 필터<br>T2Esanhtml / T2Esanclean / T2Esanregisterprovider / config/t2_sanitize.php | 없음 — config/t2_sanitize.php 404, 심볼 0/173파일 | 본문 저장 지점에서 거를 코드가 없음. §2 참고 |
§7 발행 본문 스타일 번들<br>T2Econtentstyletag / T2Econtentstyleattach / ?bundle=content&scope=&format=&v=지문 | 없음 — 심볼 0/173파일, bundle·scope 인자 0건 | §3 참고 (수동 링크가 정답) |
§2.5 설치 진단 화면<br>endpoints/install-check.php | 404 — endpoints/ 디렉터리 자체가 없음 | URL 자동 감지가 빗나갔을 때 볼 공식 진단 화면이 배포판에 없음 |
§2.6·§5 하드 설정 계약<br>config/t2_hard_config.php / admin/sections/ | 404 / admin/sections/ 없음 | 설정은 관리자 화면 + DB 행. 새 설정 키를 알려 주는 장치도 없음 |
§8 호스트 보안 브리지<br>T2Ehostsecuritysnapshot() / config/t2_cms_security.php | 404 / 심볼 0건 | 기능 손실이 아니라 관측 가능성 손실 — CMS가 무엇을 방어하는지 조회할 수 없음 |
§6.5 sanitize 설정 블록 | t2_sanitize 0건 | 켜고 끌 설정 자체가 없음 |
4-2 · v10의 t2_css_min.php 는 다른 프로그램입니다
S2 guide.txt §7 은 endpoints/t2_css_min.php?bundle=content&format=js 로 안내합니다. v10에는 endpoints/ 가 없습니다. v10의 t2_css_min.php 는 단일 CSS 파일 축소기입니다:
php// t2_css_min.php:16 $file = isset($_GET['f']) ? $_GET['f'] : '';
?f= 인자 하나만 받습니다. bundle·scope·format 인자가 없고 지문(v=)도 없습니다. ⇒ v10 사용자에게 ?bundle=content 를 안내하면 400이 나옵니다(t2_css_min.php:20-23).
4-3 · v11에 있고 v10에도 있는 것 (경계 표시)
| v10 | 근거 | |
|---|---|---|
| iframe 허용 호스트 allowlist | ✅ 있음 | config/t2_config.php:304-340 |
| 다크 모드 | ✅ 있음 | css/dark.css (6,476 B) |
| 4종 CMS 어댑터 | ✅ 있음 | integration/cms/adapters/ 4종 |
| 4언어 i18n | ✅ 있음 | locales/ + plugin/*/locales/ + js/i18n.js |
| 접근성 속성 | ✅ 부분 | aria-label 다수, editor.core.php:196 으로 다국어화 |
5. 실무 체크리스트
관련 문서: N-02-admin-console-guide.md · N-05-cms-adapter-and-data-paths.md · 74-security-guide.md · 75-upload-and-media-pipeline-guide.md
스탠데얼론이라면
- 본문 저장 지점에 HTML 정제를 붙이십시오(§2-3). 지금 안 붙이면 그 축은 비어 있습니다.
- 게시 페이지 스킨
<head>에css/content.css를 수동으로 링크하십시오(§3-3). - 배포판 루트에
readme.txt가 있고 유효일이 2026년 6월 29일인지 확인하십시오(07-license-guide.md).N-07은 채워지지 않은 번호입니다 — 이 항목에 쓸 별도 문서는 없습니다.
그누보드5·라이믹스라면
- CMS 필터가 켜져 있는지 CMS 설정에서 확인하십시오. T2Editor 화면에는 그 정보가 없습니다.
- 게시 스킨
<head>에css/content.css를 수동 링크하십시오. - iframe이 안 보이면 관리자 도메인 설정에서 해당 플랫폼을 체크하십시오(N-02-admin-console-guide.md).
- v10.5.1로 올리십시오 — 10.5.0의 CMS 인증 결함이 있습니다.
어디서든 하지 말아야 할 것
- ❌ v11 문서의
T2Econtentstyletag()·T2Econtentstyleattach()안내를 v10 서버에 쓰지 말 것 — 0건입니다. - ❌
endpoints/t2_css_min.php?bundle=content을 쓰지 말 것 — 404/400입니다. - ❌
T2Esanhtml()를 부르지 말 것 — 정의가 없습니다(fatal). - ❌ "v10은 서버에서 XSS를 막아 준다"고 쓰지 말 것 — 그 문장은 v11의 설명입니다.
N-xx 번호 체계 메모 (2026-09-28 확정)
이 위키의 N- 번호는 2026-09-28에 등록부를 확정하면서 붙였습니다. 확정된 신규 문서는 4건입니다.
| 번호 | 슬러그 | 내용 |
|---|---|---|
| N-02 | N-02-admin-console-guide.md | 통합 관리 페이지 가이드 |
| N-03 | N-03-migration-v9-to-v10.md | v9 → v10 이관 안내서 |
| N-05 | N-05-cms-adapter-and-data-paths.md | CMS 어댑터와 데이터 저장 위치 |
| N-06 | N-06-server-sanitize-and-content-style.md | 서버측 본문 정제와 게시 화면 스타일 |
N-01 · N-04 · N-07은 비어 있습니다. 번호를 미리 예약하면 그 번호가 "예정된 문서가 있다"는 뜻으로 읽히고, 실제로 그런 문서가 생기지 않으면 독자가 찾다 포기합니다. 필요해지는 시점에 그때 번호를 쓰십시오. 이전 문안에서 [[N-02-admin-console-guide.md]]·[[N-05-cms-adapter-and-data-paths.md]]처럼 가리키던 링크는 이 등록부 기준으로 교정되었습니다(N-08 → N-02, 라이믹스 경로는 N-05에 포함).
6. 내가 틀릴 수 있는 것 3가지
- "CMS 필터가 v10 본문을 거른다"는 서술을 §2-1 에서 좁혔지만, CMS 내부 동작은 v10 배포판 밖입니다. 그누보드5·라이믹스 소스를 이 세션에서 읽지 않았습니다. "CMS가 자기 경로에서 필터를 돈다"는 CMS의 알려진 성질이지, 제가 v10과 결합해 재현한 결과가 아닙니다. CMS 버전에 따라 필터가 꺼져 있거나 정책이 다를 수 있습니다.
- §3-3 의 "수동 링크가 v10의 정답"은 코드에서 확인된 것(어댑터가 head 에 주입하지 않는다)과 운영 판단이 합쳐진 결론입니다. 본문 끝 링크가 실제 CMS에서 살아남는지는 CMS·스킨·필터 설정에 달려 있고, 저는 그 CMS 위에서 재현하지 못했습니다. 그래서 "무조건 지워진다"가 아니라 "지워질 수 있으니 안전 장치로 두라"고 적었습니다.
- §1 의 심볼 0건은
releases/v10.5.1290 blob 중 php 86 + js 86 + inc 1 = 173개 파일 기준입니다. 나머지 117개(json76 ·css19 ·txt5 ·htaccess3 ·html3 ·woff22 ·wasm3 ·bin2 ·xml1 · 확장자 없음 3)는 심볼 검색에서 제외했습니다. 제외 근거는 가정입니다(이미지에 심볼이 있을 수는 없지만, 텍스트 파일에 주석으로 새어 있을 가능성은 배제하지 않습니다) —[확인 필요]. 그리고 배포 ZIP이 git tree와 같은 파일 집합이라는 보장은 없습니다. ZIP에 v11 스타일의 추가 파일이 들어 있을 가능성은 배제하지 않았습니다.download_url오염 금지 규칙 때문에 열지 않았습니다.