KISA 신고포상제 접수 3건(KVE-2026-1677/1678/1679)의 공통 원인은 하나다 — "위험한 것을 골라 막았고, 목록 밖으로 우회됐다". 세 지점 모두 판정을 뒤집는다. 예약 작업 Artisan (KVE-2026-1679) 차단 9개 / 허용 229개였던 구조를 허용목록으로 바꾸고 명령마다 허용 옵션을 선언한다. 검증 과정에서 보고서보다 짧은 경로를 추가로 발견했다 — 검증기는 preg_split 의 첫 토큰을 명령명으로 봤지만 실행부는 문자열째 넘겼고, parameters 가 비면 Laravel 이 StringInput 으로 재파싱한다. 따옴표 한 쌍이면 차단목록의 tinker 가 그대로 실행됐다 (마커 파일로 확인). 파서를 엄격 형태 하나로 고정하고 실행은 (명령명, 인자배열)로 넘겨 재해석 지점 자체를 없앴다. 언어팩 (KVE-2026-1678) 위험 함수 11개를 정규식으로 나열하던 검사를 토큰 상태기계로 교체해 "번역 배열만" 통과시킨다. 목록에 없는 함수도, 함수 이름을 아예 쓰지 않는 형태도 원리적으로 막힌다. 파일 형식·PHP 위치·심볼릭 링크도 화이트리스트로 뒤집었다. 활성 승격이 require 배선의 필수 조건이므로 설치와 활성화의 권한 경계를 네 설치 경로 전부에 같은 강도로 건다 — 경로별 면제는 곧 우회로가 되기 때문이다. 화면은 운영자의 활성화 권한을 보고 auto_activate 를 실을지 결정하므로 재설치 흐름은 그대로 동작한다. 인스톨러 (KVE-2026-1677) 실행 바이너리 자리에 인자가 들어가는 것을 형태 규칙으로 차단한다. escapeshellarg 는 "하나의 인자" 임만 보장할 뿐 그 인자가 실행 파일인지 스크립트인지는 보장하지 않는다. 판정을 의존성 0 공용 정책으로 모아 진단 API·설치 워커·저장 시점이 같은 규칙을 쓴다. stat 은 호출하지 않는다 — 접근이 제한된 서버에서 정상 경로가 거부되던 회귀를 재발시키지 않기 위함이다. 검증: 백엔드 392건 + 프론트 7건 green(skip 1, symlink 미지원 환경). PoC 마커로 red 를 먼저 확보하고, 수정 후 stash 역회귀로 다시 red 가 되는 것까지 확인했다.
68 lines
5.6 KiB
PHP
68 lines
5.6 KiB
PHP
<?php
|
|
|
|
return [
|
|
'fetch_success' => '언어팩 목록을 조회했습니다.',
|
|
'fetch_failed' => '언어팩 목록을 불러오지 못했습니다: :error',
|
|
'not_found' => '언어팩을 찾을 수 없습니다.',
|
|
'install_success' => '언어팩을 설치했습니다.',
|
|
'install_failed' => '언어팩 설치에 실패했습니다: :error',
|
|
'activate_success' => '언어팩을 활성화했습니다.',
|
|
'activate_failed' => '언어팩 활성화에 실패했습니다: :error',
|
|
'deactivate_success' => '언어팩을 비활성화했습니다.',
|
|
'deactivate_failed' => '언어팩 비활성화에 실패했습니다: :error',
|
|
'uninstall_success' => '언어팩을 제거했습니다.',
|
|
'uninstall_failed' => '언어팩 제거에 실패했습니다: :error',
|
|
'manifest_invalid' => 'language-pack.json 검증에 실패했습니다.',
|
|
'check_updates_success' => '업데이트 확인을 완료했습니다.',
|
|
'check_updates_failed' => '업데이트 확인에 실패했습니다: :error',
|
|
'update_success' => '언어팩을 업데이트했습니다.',
|
|
'update_failed' => '언어팩 업데이트에 실패했습니다: :error',
|
|
'refresh_cache_success' => '언어팩 캐시를 갱신했습니다.',
|
|
'refresh_cache_failed' => '언어팩 캐시 갱신에 실패했습니다: :error',
|
|
'preview_success' => 'manifest 미리보기를 완료했습니다.',
|
|
'preview_failed' => 'manifest 미리보기에 실패했습니다: :error',
|
|
|
|
'errors' => [
|
|
'manifest_not_found' => 'ZIP 안에서 language-pack.json 파일을 찾을 수 없습니다.',
|
|
'manifest_invalid_json' => 'language-pack.json 의 JSON 형식이 올바르지 않습니다.',
|
|
'php_outside_backend' => 'PHP 파일은 backend/{언어코드}/{그룹}.php 위치에만 둘 수 있습니다 (:file).',
|
|
'php_file_not_literal_array' => 'PHP 파일에는 번역 배열만 담을 수 있습니다. :file 파일 :line 번째 줄에 배열이 아닌 코드가 있습니다.',
|
|
'unsupported_file_type' => '언어팩에 넣을 수 없는 파일 형식입니다 (:file — :extension). json, php, md 파일만 포함할 수 있습니다.',
|
|
'symlink_not_allowed' => '언어팩에는 심볼릭 링크를 포함할 수 없습니다 (:file).',
|
|
'auto_activate_requires_manage' => '설치 후 바로 켜기는 언어팩 관리 권한이 필요합니다. 이 항목을 빼면 설치는 그대로 진행됩니다.',
|
|
'core_locale_missing' => '코어 언어팩(:locale)이 먼저 활성화되어야 합니다.',
|
|
'target_not_installed' => '대상 :scope (:target)이 설치되어 있지 않습니다.',
|
|
'target_inactive' => '대상 :scope (:target)이 비활성 상태입니다. 먼저 활성화해야 합니다.',
|
|
'target_version_too_old' => '대상 :scope (:target) 버전이 요구 조건(:constraint)을 충족하지 않습니다.',
|
|
'downgrade_blocked' => '다운그레이드가 차단되었습니다 (:from → :to).',
|
|
'protected_pack' => '보호된 언어팩은 비활성화/제거할 수 없습니다.',
|
|
'download_failed' => 'URL(:url) 에서 언어팩을 다운로드하지 못했습니다: :error',
|
|
'download_url_not_public' => '내부 네트워크 주소(사설 IP·localhost 등)에서는 언어팩을 내려받을 수 없습니다. 외부에서 접속 가능한 https 주소를 사용해주세요.',
|
|
'checksum_mismatch' => '체크섬이 일치하지 않습니다.',
|
|
'update_no_source' => '업데이트 소스 정보가 없습니다 (GitHub 소스 언어팩만 업데이트 가능).',
|
|
'update_already_latest' => '이미 최신 버전입니다.',
|
|
'preview_invalid_manifest' => 'manifest 파일이 유효하지 않아 미리보기에 실패했습니다.',
|
|
'operation_in_progress' => '언어팩(:identifier) 에서 다른 작업이 진행 중입니다. 완료 후 다시 시도해주세요.',
|
|
'bundled_not_found' => '번들 디렉토리(lang-packs/_bundled/:identifier) 가 존재하지 않습니다.',
|
|
'slot_conflict' => '같은 슬롯에 이미 활성 언어팩(:current)이 있습니다. (:target)으로 교체하려면 확인이 필요합니다.',
|
|
'directory_not_writable' => '언어팩 설치 디렉토리(:path) 에 쓰기 권한이 없습니다. 웹 서버 사용자(예: www-data) 에게 쓰기 권한을 부여하세요. (예: sudo chgrp www-data :path && sudo chmod g+w :path)',
|
|
'identifier_not_found' => '언어팩을 찾을 수 없습니다 (:identifier).',
|
|
'unsupported_source' => '지원하지 않는 소스 (:source) — bundled, github, url 중 하나를 사용하세요.',
|
|
],
|
|
|
|
'validation' => [
|
|
'file_required' => '언어팩 ZIP 파일을 업로드하세요.',
|
|
'file_invalid' => '업로드된 파일이 유효하지 않습니다.',
|
|
'file_too_large' => '파일 크기는 10MB 를 초과할 수 없습니다.',
|
|
'file_not_zip' => 'ZIP 형식의 파일만 업로드할 수 있습니다.',
|
|
'github_url_required' => 'GitHub URL 을 입력하세요.',
|
|
'github_url_invalid' => '올바른 URL 을 입력하세요.',
|
|
'github_url_format' => 'GitHub 저장소 URL 형식이어야 합니다 (예: https://github.com/owner/repo).',
|
|
'url_required' => '다운로드 URL 을 입력하세요.',
|
|
'url_invalid' => '올바른 URL 을 입력하세요.',
|
|
'checksum_invalid' => 'SHA-256 체크섬은 64자 16진수여야 합니다.',
|
|
'identifier_required' => '언어팩 식별자를 입력하세요.',
|
|
'identifier_invalid' => '식별자는 영문/숫자/점/언더스코어/하이픈만 허용됩니다.',
|
|
],
|
|
];
|