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
4.7 KiB
PHP
68 lines
4.7 KiB
PHP
<?php
|
|
|
|
return [
|
|
'fetch_success' => 'Language pack list retrieved.',
|
|
'fetch_failed' => 'Failed to load language pack list: :error',
|
|
'not_found' => 'Language pack not found.',
|
|
'install_success' => 'Language pack installed.',
|
|
'install_failed' => 'Failed to install language pack: :error',
|
|
'activate_success' => 'Language pack activated.',
|
|
'activate_failed' => 'Failed to activate language pack: :error',
|
|
'deactivate_success' => 'Language pack deactivated.',
|
|
'deactivate_failed' => 'Failed to deactivate language pack: :error',
|
|
'uninstall_success' => 'Language pack removed.',
|
|
'uninstall_failed' => 'Failed to remove language pack: :error',
|
|
'manifest_invalid' => 'language-pack.json validation failed.',
|
|
'check_updates_success' => 'Update check completed.',
|
|
'check_updates_failed' => 'Failed to check updates: :error',
|
|
'update_success' => 'Language pack updated.',
|
|
'update_failed' => 'Failed to update language pack: :error',
|
|
'refresh_cache_success' => 'Language pack cache refreshed.',
|
|
'refresh_cache_failed' => 'Failed to refresh language pack cache: :error',
|
|
'preview_success' => 'Manifest preview completed.',
|
|
'preview_failed' => 'Failed to preview manifest: :error',
|
|
|
|
'errors' => [
|
|
'manifest_not_found' => 'language-pack.json file not found in archive.',
|
|
'manifest_invalid_json' => 'language-pack.json contains invalid JSON.',
|
|
'php_outside_backend' => 'PHP files are only allowed at backend/{locale}/{group}.php (:file).',
|
|
'php_file_not_literal_array' => 'PHP files may only contain a translation array. Line :line of :file contains code that is not part of an array.',
|
|
'unsupported_file_type' => 'This file type cannot be included in a language pack (:file — :extension). Only json, php and md files are allowed.',
|
|
'symlink_not_allowed' => 'Language packs cannot contain symbolic links (:file).',
|
|
'auto_activate_requires_manage' => 'Activating the pack right after installation requires the language pack management permission. Installation proceeds normally without this option.',
|
|
'core_locale_missing' => 'A core language pack for ":locale" must be active first.',
|
|
'target_not_installed' => 'Target :scope (":target") is not installed.',
|
|
'target_inactive' => 'Target :scope (":target") is inactive. Activate it first.',
|
|
'target_version_too_old' => 'Target :scope (":target") version does not satisfy the required constraint (:constraint).',
|
|
'downgrade_blocked' => 'Downgrade blocked (:from → :to).',
|
|
'protected_pack' => 'Protected language packs cannot be deactivated or removed.',
|
|
'download_failed' => 'Failed to download from URL: :url',
|
|
'download_url_not_public' => 'Language packs cannot be downloaded from internal network addresses (private IPs, localhost, etc.). Use a publicly reachable https address.',
|
|
'checksum_mismatch' => 'Checksum mismatch.',
|
|
'update_no_source' => 'No update source available (only GitHub-sourced packs can be updated).',
|
|
'update_already_latest' => 'Already on the latest version.',
|
|
'preview_invalid_manifest' => 'Manifest preview failed: invalid manifest file.',
|
|
'operation_in_progress' => 'Another operation is in progress for language pack (:identifier). Please try again after it completes.',
|
|
'bundled_not_found' => 'Bundled directory (lang-packs/_bundled/:identifier) does not exist.',
|
|
'slot_conflict' => 'Slot already has an active language pack (:current). Confirm replacement with :target.',
|
|
'directory_not_writable' => 'The language pack install directory (:path) is not writable. Grant write permission to the web server user (e.g. www-data). Example: sudo chgrp www-data :path && sudo chmod g+w :path',
|
|
'identifier_not_found' => 'Language pack not found (:identifier).',
|
|
'unsupported_source' => 'Unsupported source (:source) — use one of bundled, github, url.',
|
|
],
|
|
|
|
'validation' => [
|
|
'file_required' => 'Upload a language pack ZIP file.',
|
|
'file_invalid' => 'Uploaded file is not valid.',
|
|
'file_too_large' => 'File size must not exceed 10 MB.',
|
|
'file_not_zip' => 'Only ZIP archives are accepted.',
|
|
'github_url_required' => 'Enter a GitHub URL.',
|
|
'github_url_invalid' => 'Enter a valid URL.',
|
|
'github_url_format' => 'Must be a GitHub repository URL (e.g. https://github.com/owner/repo).',
|
|
'url_required' => 'Enter a download URL.',
|
|
'url_invalid' => 'Enter a valid URL.',
|
|
'checksum_invalid' => 'SHA-256 checksum must be 64 hex characters.',
|
|
'identifier_required' => 'Enter the language pack identifier.',
|
|
'identifier_invalid' => 'Identifier may contain letters, numbers, dots, underscores and hyphens only.',
|
|
],
|
|
];
|