Безопасность: код установки ретранслятора (M1), zip-slip 7z (M3), L1/L5/L2
Продолжение исправлений по security-аудиту. M1 (MEDIUM) — установщик PHP-ретранслятора был открыт без входа: до создания админа атакующий из интернета мог завести админа кабинета или подменить адрес ядра/ключ. Добавлен «код установки»: генерируется в файл install.lock рядом с данными (вне веб-корня), прочитать его можно только с доступом к файловой системе (FTP/файловый менеджер) — то есть владельцу сервера. Шаги connect и admin (включая «пропустить») требуют верный код. Регексп loopback заякорен (`http://127.0.0.1.attacker.com` больше не проходит как loopback). Код удаляется по завершении установки. Проверено встроенным php-сервером. M3 (MEDIUM) — распаковка 7z в мастере ключа (ExtractISO) сохраняла пути из архива → запись «../…» вышла бы за каталог. Перед распаковкой архив просматривается (7z l -slt, без записи); при абсолютном пути или выходе за корень распаковка отклоняется. Тест TestUnsafeArchivePath. L1 — target_device не проверялся перед root mount: значение не из списка съёмных теперь отклоняется (иначе root смонтировал бы произвольный девайс). L5 — likeEscape в ручном поиске DEPO (manualDepoSearch): метасимволы LIKE от пользователя больше не «расползаются» по базе. L2a — кука сессии ядра получает Secure и за TLS-терминирующим прокси на loopback (reqIsHTTPS учитывает X-Forwarded-Proto только от loopback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -290,6 +291,16 @@ func ExtractISO(ctx context.Context, isoPath, password string) (Medium, error) {
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
// Защита от zip-slip: 7z x сохраняет пути из архива, поэтому запись «../…» ушла
|
||||
// бы за dst. Сначала СПИСОК архива (не пишет на диск) — если есть абсолютный путь
|
||||
// или выход за корень, распаковку не выполняем.
|
||||
if bad, err := archiveUnsafePath(cctx, abs, password); err != nil {
|
||||
_ = os.RemoveAll(dst)
|
||||
return Medium{}, err
|
||||
} else if bad != "" {
|
||||
_ = os.RemoveAll(dst)
|
||||
return Medium{}, fmt.Errorf("архив отклонён: небезопасный путь внутри (%s)", bad)
|
||||
}
|
||||
// 7z x -y -o<dst> [-p<pass>] <archive> — рекурсивное извлечение.
|
||||
args := []string{"x", "-y", "-o" + dst}
|
||||
if password != "" {
|
||||
@@ -313,6 +324,53 @@ func ExtractISO(ctx context.Context, isoPath, password string) (Medium, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// archiveUnsafePath — просматривает СПИСОК архива (7z l -slt, без записи на диск)
|
||||
// и возвращает первый небезопасный путь: абсолютный или выходящий за корень («..»).
|
||||
// Пустая строка — все пути безопасны.
|
||||
func archiveUnsafePath(ctx context.Context, archive, password string) (string, error) {
|
||||
args := []string{"l", "-slt", "-ba"}
|
||||
if password != "" {
|
||||
args = append(args, "-p"+password)
|
||||
} else {
|
||||
args = append(args, "-p-")
|
||||
}
|
||||
args = append(args, archive)
|
||||
out, err := exec.CommandContext(ctx, "7z", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("7z l: %w / %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
v, ok := strings.CutPrefix(line, "Path = ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(v)
|
||||
if p != "" && unsafeArchivePath(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// unsafeArchivePath — путь из архива опасен: абсолютный (Unix/Windows) или
|
||||
// выходящий за корень распаковки хотя бы одним компонентом «..».
|
||||
func unsafeArchivePath(p string) bool {
|
||||
if strings.HasPrefix(p, "/") || strings.HasPrefix(p, `\`) || (len(p) >= 2 && p[1] == ':') {
|
||||
return true
|
||||
}
|
||||
norm := strings.ReplaceAll(p, `\`, "/")
|
||||
if clean := path.Clean(norm); clean == ".." || strings.HasPrefix(clean, "../") {
|
||||
return true
|
||||
}
|
||||
for _, seg := range strings.Split(norm, "/") {
|
||||
if seg == ".." {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isoID(absPath string) string {
|
||||
h := sha256.Sum256([]byte(absPath))
|
||||
return hex.EncodeToString(h[:8])
|
||||
|
||||
Reference in New Issue
Block a user