|
22 | 22 | from web.sql_safety import safe_table, safe_columns, build_update |
23 | 23 | import web.state as _state |
24 | 24 | from web.utils import clone_json_fallback as _clone_json_fallback, safe_json_list as _safe_json_list, validate_download_url as _validate_download_url |
| 25 | +import posixpath as _posixpath |
| 26 | + |
| 27 | + |
| 28 | +def _sanitize_folder(raw): |
| 29 | + """Sanitize a user-supplied folder path for media organization. |
| 30 | +
|
| 31 | + Collapses traversal sequences, rejects anything that escapes the root, |
| 32 | + and strips leading/trailing slashes. Returns a clean relative path or ''. |
| 33 | + """ |
| 34 | + if not raw: |
| 35 | + return '' |
| 36 | + # Normalize using posixpath to collapse .. and . regardless of OS |
| 37 | + normed = _posixpath.normpath(raw.replace('\\', '/')) |
| 38 | + # Reject any path that escapes root (starts with .. or is absolute) |
| 39 | + if normed.startswith('..') or normed.startswith('/'): |
| 40 | + return '' |
| 41 | + # Strip surrounding slashes and dots for safety |
| 42 | + return normed.strip('/').strip('.') |
25 | 43 |
|
26 | 44 | try: |
27 | 45 | from web.catalog import CHANNEL_CATALOG, CHANNEL_CATEGORIES |
@@ -409,7 +427,7 @@ def api_videos_upload(): |
409 | 427 | file.save(filepath) |
410 | 428 | filesize = os.path.getsize(filepath) if os.path.isfile(filepath) else 0 |
411 | 429 | category = request.form.get('category', 'general') |
412 | | - folder = request.form.get('folder', '').replace('..', '').strip('/') |
| 430 | + folder = _sanitize_folder(request.form.get('folder', '')) |
413 | 431 | title = request.form.get('title', filename.rsplit('.', 1)[0]) |
414 | 432 | with db_session() as db: |
415 | 433 | cur = db.execute('INSERT INTO videos (title, filename, category, folder, filesize) VALUES (?, ?, ?, ?, ?)', |
@@ -1502,7 +1520,7 @@ def api_audio_upload(): |
1502 | 1520 | filesize = os.path.getsize(filepath) if os.path.isfile(filepath) else 0 |
1503 | 1521 | title = request.form.get('title', filename.rsplit('.', 1)[0]) |
1504 | 1522 | category = request.form.get('category', 'general') |
1505 | | - folder = request.form.get('folder', '').replace('..', '').strip('/') |
| 1523 | + folder = _sanitize_folder(request.form.get('folder', '')) |
1506 | 1524 | artist = request.form.get('artist', '') |
1507 | 1525 | with db_session() as db: |
1508 | 1526 | cur = db.execute('INSERT INTO audio (title, filename, category, folder, artist, filesize) VALUES (?, ?, ?, ?, ?, ?)', |
@@ -2091,7 +2109,7 @@ def api_books_upload(): |
2091 | 2109 | title = request.form.get('title', filename.rsplit('.', 1)[0]) |
2092 | 2110 | author = request.form.get('author', '') |
2093 | 2111 | category = request.form.get('category', 'general') |
2094 | | - folder = request.form.get('folder', '').replace('..', '').strip('/') |
| 2112 | + folder = _sanitize_folder(request.form.get('folder', '')) |
2095 | 2113 | with db_session() as db: |
2096 | 2114 | cur = db.execute('INSERT INTO books (title, author, filename, format, category, folder, filesize) VALUES (?, ?, ?, ?, ?, ?, ?)', |
2097 | 2115 | (title, author, filename, fmt, category, folder, filesize)) |
|
0 commit comments