authentik.admin.files.validation

 1import re
 2from pathlib import PurePosixPath
 3
 4from django.core.exceptions import ValidationError
 5from django.utils.translation import gettext as _
 6
 7from authentik.admin.files.backends.base import THEME_VARIABLE
 8from authentik.admin.files.backends.passthrough import PassthroughBackend
 9from authentik.admin.files.backends.static import StaticBackend
10from authentik.admin.files.usage import FileUsage
11
12# File upload limits
13MAX_FILE_NAME_LENGTH = 1024
14MAX_PATH_COMPONENT_LENGTH = 255
15
16
17def validate_file_name(name: str) -> None:
18    if PassthroughBackend(FileUsage.MEDIA).supports_file(name) or StaticBackend(
19        FileUsage.MEDIA
20    ).supports_file(name):
21        return
22    validate_upload_file_name(name)
23
24
25def validate_upload_file_name(
26    name: str,
27    ValidationError: type[Exception] = ValidationError,
28) -> None:
29    """Sanitize file path.
30
31    Args:
32        file_path: The file path to sanitize
33
34    Returns:
35        Sanitized file path
36
37    Raises:
38        ValidationError: If file path is invalid
39    """
40    if not name:
41        raise ValidationError(_("File name cannot be empty"))
42
43    # Allow %(theme)s placeholder for theme-specific files
44    # Replace with placeholder for validation, then check the result
45    name_for_validation = name.replace(THEME_VARIABLE, "theme")
46
47    # Same regex is used in the frontend as well (with %(theme)s handling)
48    if not re.match(r"^[a-zA-Z0-9._/-]+$", name_for_validation):
49        raise ValidationError(
50            _(
51                "File name can only contain letters (a-z, A-Z), numbers (0-9), "
52                "dots (.), hyphens (-), underscores (_), forward slashes (/), "
53                "and the placeholder %(theme)s for theme-specific files"
54            )
55        )
56
57    if "//" in name:
58        raise ValidationError(_("File name cannot contain duplicate /"))
59
60    # Convert to posix path
61    path = PurePosixPath(name)
62
63    # Check for absolute paths
64    # Needs the / at the start. If it doesn't have it, it might still be unsafe, so see L53+
65    if path.is_absolute():
66        raise ValidationError(_("Absolute paths are not allowed"))
67
68    # Check for parent directory references
69    if ".." in path.parts:
70        raise ValidationError(_("Parent directory references ('..') are not allowed"))
71
72    # Disallow paths starting with dot (hidden files at root level)
73    if str(path).startswith("."):
74        raise ValidationError(_("Paths cannot start with '.'"))
75
76    # Check path length limits
77    normalized = str(path)
78    if len(normalized) > MAX_FILE_NAME_LENGTH:
79        raise ValidationError(_(f"File name too long (max {MAX_FILE_NAME_LENGTH} characters)"))
80
81    for part in path.parts:
82        if len(part) > MAX_PATH_COMPONENT_LENGTH:
83            raise ValidationError(
84                _(f"Path component too long (max {MAX_PATH_COMPONENT_LENGTH} characters)")
85            )
MAX_FILE_NAME_LENGTH = 1024
MAX_PATH_COMPONENT_LENGTH = 255
def validate_file_name(name: str) -> None:
18def validate_file_name(name: str) -> None:
19    if PassthroughBackend(FileUsage.MEDIA).supports_file(name) or StaticBackend(
20        FileUsage.MEDIA
21    ).supports_file(name):
22        return
23    validate_upload_file_name(name)
def validate_upload_file_name( name: str, ValidationError: type[Exception] = <class 'django.core.exceptions.ValidationError'>) -> None:
26def validate_upload_file_name(
27    name: str,
28    ValidationError: type[Exception] = ValidationError,
29) -> None:
30    """Sanitize file path.
31
32    Args:
33        file_path: The file path to sanitize
34
35    Returns:
36        Sanitized file path
37
38    Raises:
39        ValidationError: If file path is invalid
40    """
41    if not name:
42        raise ValidationError(_("File name cannot be empty"))
43
44    # Allow %(theme)s placeholder for theme-specific files
45    # Replace with placeholder for validation, then check the result
46    name_for_validation = name.replace(THEME_VARIABLE, "theme")
47
48    # Same regex is used in the frontend as well (with %(theme)s handling)
49    if not re.match(r"^[a-zA-Z0-9._/-]+$", name_for_validation):
50        raise ValidationError(
51            _(
52                "File name can only contain letters (a-z, A-Z), numbers (0-9), "
53                "dots (.), hyphens (-), underscores (_), forward slashes (/), "
54                "and the placeholder %(theme)s for theme-specific files"
55            )
56        )
57
58    if "//" in name:
59        raise ValidationError(_("File name cannot contain duplicate /"))
60
61    # Convert to posix path
62    path = PurePosixPath(name)
63
64    # Check for absolute paths
65    # Needs the / at the start. If it doesn't have it, it might still be unsafe, so see L53+
66    if path.is_absolute():
67        raise ValidationError(_("Absolute paths are not allowed"))
68
69    # Check for parent directory references
70    if ".." in path.parts:
71        raise ValidationError(_("Parent directory references ('..') are not allowed"))
72
73    # Disallow paths starting with dot (hidden files at root level)
74    if str(path).startswith("."):
75        raise ValidationError(_("Paths cannot start with '.'"))
76
77    # Check path length limits
78    normalized = str(path)
79    if len(normalized) > MAX_FILE_NAME_LENGTH:
80        raise ValidationError(_(f"File name too long (max {MAX_FILE_NAME_LENGTH} characters)"))
81
82    for part in path.parts:
83        if len(part) > MAX_PATH_COMPONENT_LENGTH:
84            raise ValidationError(
85                _(f"Path component too long (max {MAX_PATH_COMPONENT_LENGTH} characters)")
86            )

Sanitize file path.

Args: file_path: The file path to sanitize

Returns: Sanitized file path

Raises: ValidationError: If file path is invalid