Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -10,6 +10,7 @@ Kept out of the CLI and routes so both share one implementation:
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -532,6 +533,30 @@ def store_inline_payload(entry, filename, contenttype, rawbytes):
|
||||
return payload
|
||||
|
||||
|
||||
# A scope name becomes a DIRECTORY on the imaging share, so it has to be a
|
||||
# single safe path segment. It was validated only as "not empty", which meant a
|
||||
# name like "../escape" or an absolute path wrote the manifest outside the
|
||||
# share root - proven by writing /tmp/manifest.json and files one and two
|
||||
# levels above it. The real exposure is the permission split: geenforce.manage
|
||||
# creates the scope, geenforce.publish writes the file, so a manage-only author
|
||||
# could aim a publisher's write anywhere that process can reach.
|
||||
#
|
||||
# Allowed: letters, digits, dot, dash, underscore. Not '.' or '..', and no
|
||||
# separator of either platform - the share is written from Linux and read by
|
||||
# Windows, so a backslash is a separator too.
|
||||
SCOPENAME_PATTERN = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$')
|
||||
|
||||
|
||||
def valid_scopename(scopename):
|
||||
"""True when this name is safe to use as a directory on the share."""
|
||||
name = (scopename or '').strip()
|
||||
if not name or name in ('.', '..') or len(name) > 100:
|
||||
return False
|
||||
if '/' in name or '\\' in name or '\x00' in name:
|
||||
return False
|
||||
return bool(SCOPENAME_PATTERN.match(name))
|
||||
|
||||
|
||||
def export_scope_to_share(scopename, phase, shareroot):
|
||||
"""Write a scope's current published JSON to the share, backing up the old
|
||||
file to _meta/history first. Returns the written path."""
|
||||
@@ -546,7 +571,21 @@ def export_scope_to_share(scopename, phase, shareroot):
|
||||
if phase == 'preinstall':
|
||||
target = os.path.join(shareroot, 'preinstall.json')
|
||||
else:
|
||||
# Checked again here, not only at creation: rows predating the
|
||||
# validation exist, and this is the line that actually writes.
|
||||
if not valid_scopename(scopename):
|
||||
raise ValueError(
|
||||
f'Refusing to export scope {scopename!r}: the name is not a '
|
||||
f'safe directory name')
|
||||
target = os.path.join(shareroot, scopename, 'manifest.json')
|
||||
|
||||
# Belt and braces: whatever the name, the resolved target must sit inside
|
||||
# the share root.
|
||||
root = os.path.realpath(shareroot)
|
||||
if os.path.commonpath([root, os.path.realpath(os.path.dirname(target))]) != root:
|
||||
raise ValueError(
|
||||
f'Refusing to export scope {scopename!r}: it resolves outside the '
|
||||
f'share root')
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
|
||||
if os.path.isfile(target):
|
||||
|
||||
Reference in New Issue
Block a user