falcon icon indicating copy to clipboard operation
falcon copied to clipboard

Make `secure_filename()` escape reserved Windows filenames

Open vytas7 opened this issue 1 year ago • 6 comments

@CaselIT noticed that we didn't hold Windows hand regarding device file names, like https://github.com/pallets/werkzeug/blob/7868bef5d978093a8baa0784464ebe5d775ae92a/src/werkzeug/utils.py#L229-L237

(Originally posted by @CaselIT in https://github.com/falconry/falcon/pull/2421#discussion_r1887617832.)

vytas7 avatar Dec 20 '24 09:12 vytas7

I was reading a bit about this, and it is actually possible to both create and delete such files on Windows by using proper escaping. Maybe just a documentation note would be enough? I don't expect many to deploy Falcon on a Windows server in production, but of course you never know.

If we do decide to implement this check, we ought to restrict this behaviour to the Windows platform to make sure we avoid a breaking change elsewhere.

See also: os.path.isreserved(...) (Python 3.13+ needed).

vytas7 avatar Dec 20 '24 09:12 vytas7

just documenting that special file names in windows are not handled is likely fine

CaselIT avatar Dec 21 '24 16:12 CaselIT

This is what is defined in the stdlib's (3.13) ntpath.py:

_reserved_chars = frozenset(
    {chr(i) for i in range(32)} |
    {'"', '*', ':', '<', '>', '?', '|', '/', '\\'}
)

_reserved_names = frozenset(
    {'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
    {f'COM{c}' for c in '123456789\xb9\xb2\xb3'} |
    {f'LPT{c}' for c in '123456789\xb9\xb2\xb3'}
)

def isreserved(path):
    """Return true if the pathname is reserved by the system."""
    # Refer to "Naming Files, Paths, and Namespaces":
    # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
    path = os.fsdecode(splitroot(path)[2]).replace(altsep, sep)
    return any(_isreservedname(name) for name in reversed(path.split(sep)))

def _isreservedname(name):
    """Return true if the filename is reserved by the system."""
    # Trailing dots and spaces are reserved.
    if name[-1:] in ('.', ' '):
        return name not in ('.', '..')
    # Wildcards, separators, colon, and pipe (*?"<>/\:|) are reserved.
    # ASCII control characters (0-31) are reserved.
    # Colon is reserved for file streams (e.g. "name:stream[:type]").
    if _reserved_chars.intersection(name):
        return True
    # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules
    # are complex and vary across Windows versions. On the side of
    # caution, return True for names that may not be reserved.
    return name.partition('.')[0].rstrip(' ').upper() in _reserved_names

Most of the things char-related are irrelevant as we already replace them (although probably check that trailing dot case), so we could just check for these reserved names if we detect the Windows platform.

vytas7 avatar Dec 21 '24 16:12 vytas7

Hi! If this issue hasn’t been resolved yet, I’d like to start working on a fix. Please let me know.

AchilleasKat avatar Jun 24 '25 21:06 AchilleasKat

Hi @AchilleasKat! The issue is still open, go ahead!

vytas7 avatar Jun 25 '25 04:06 vytas7

Thanks for the confirmation! I’ll start working on it.

AchilleasKat avatar Jun 25 '25 19:06 AchilleasKat