Reject unsafe ZIP paths
This commit is contained in:
@@ -128,6 +128,24 @@
|
|||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
|
## Исправление traversal — 2026-09-05
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
|
||||||
|
`list_zip_files.js` использовал сырой `entryName` из ZIP: он напрямую попадал в `path` и разбивался через `split('/')`. Поэтому `..`, ведущий `/`, пустые сегменты и Windows backslash создавали видимые небезопасные группы и пути.
|
||||||
|
|
||||||
|
### Исправление
|
||||||
|
|
||||||
|
- Добавлена проверка сегментов ZIP entry до построения дерева.
|
||||||
|
- Отклоняются пустые entry, абсолютные пути, backslash, `.`, `..` и пустые сегменты.
|
||||||
|
- Пути и имена leaf-файлов строятся только из проверенных сегментов.
|
||||||
|
- Вложенный ZIP с опасным путём пропускается без прерывания обработки внешнего ZIP.
|
||||||
|
- Версия повышена до `0.1.8`.
|
||||||
|
|
||||||
|
### Повторная проверка
|
||||||
|
|
||||||
|
ZIP с `folder/../../escape.txt`, `/absolute.md`, `windows\\..\\bad.txt`, `folder/./dot.txt` и безопасным `safe/normal.txt` оставил только `paths.zip/safe/normal.txt`. Группы `..`, пустая группа и абсолютный путь больше не отображаются. Результат: `1 файлов · 2 B`.
|
||||||
|
|
||||||
- `c0b8788 Implement reusable upload platform` — первоначальный reusable upload platform.
|
- `c0b8788 Implement reusable upload platform` — первоначальный reusable upload platform.
|
||||||
- `85605fe Remove VM upload layer; keep file picker only` — удаление VM upload и переход к picker-only.
|
- `85605fe Remove VM upload layer; keep file picker only` — удаление VM upload и переход к picker-only.
|
||||||
- `a965f18 Add hierarchical file picker` — иерархическое дерево, вложенные ZIP, удаление групп и файлов.
|
- `a965f18 Add hierarchical file picker` — иерархическое дерево, вложенные ZIP, удаление групп и файлов.
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ with (ROOT / "config.json").open(encoding="utf-8") as config_file:
|
|||||||
CONFIG = json.load(config_file)
|
CONFIG = json.load(config_file)
|
||||||
|
|
||||||
|
|
||||||
VERSION = "0.1.7"
|
VERSION = "0.1.8"
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ function makeFile(data, name) {
|
|||||||
return new File([data], name);
|
return new File([data], name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeEntryParts(entryName) {
|
||||||
|
if (!entryName || entryName.startsWith('/') || entryName.includes('\\')) return null;
|
||||||
|
const parts = entryName.split('/');
|
||||||
|
if (parts.some((part) => !part || part === '.' || part === '..')) return null;
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
function node(kind, name, path, children = [], file = null) {
|
function node(kind, name, path, children = [], file = null) {
|
||||||
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: true };
|
return { id: crypto.randomUUID(), kind, name, path, children, file, expanded: true };
|
||||||
}
|
}
|
||||||
@@ -42,17 +49,20 @@ async function listEntries(data, zipName, allowedExt, depth) {
|
|||||||
|
|
||||||
for (const [entryName, entryData] of Object.entries(entries)) {
|
for (const [entryName, entryData] of Object.entries(entries)) {
|
||||||
if (entryName.endsWith('/')) continue;
|
if (entryName.endsWith('/')) continue;
|
||||||
if (entryName.toLowerCase().endsWith('.zip')) {
|
const parts = safeEntryParts(entryName);
|
||||||
|
if (!parts) continue;
|
||||||
|
const normalizedName = parts.join('/');
|
||||||
|
if (normalizedName.toLowerCase().endsWith('.zip')) {
|
||||||
const nested = await listEntries(entryData, entryName, allowedExt, depth + 1);
|
const nested = await listEntries(entryData, entryName, allowedExt, depth + 1);
|
||||||
if (nested) {
|
if (nested) {
|
||||||
rebaseTree(nested, zipName);
|
rebaseTree(nested, zipName);
|
||||||
nested.name = entryName.split('/').pop();
|
nested.name = parts.at(-1);
|
||||||
addPath(root, entryName.split('/'), nested);
|
addPath(root, parts, nested);
|
||||||
}
|
}
|
||||||
} else if (extensionAllowed(entryName, allowedExt)) {
|
} else if (extensionAllowed(normalizedName, allowedExt)) {
|
||||||
const path = `${zipName}/${entryName}`;
|
const path = `${zipName}/${normalizedName}`;
|
||||||
const file = makeFile(entryData, path);
|
const file = makeFile(entryData, path);
|
||||||
addPath(root, entryName.split('/'), node('file', entryName.split('/').pop(), path, [], file));
|
addPath(root, parts, node('file', parts.at(-1), path, [], file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return root.children.length ? root : null;
|
return root.children.length ? root : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user