Make `secure_filename()` escape reserved Windows filenames
@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.)
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).
just documenting that special file names in windows are not handled is likely fine
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.
Hi! If this issue hasn’t been resolved yet, I’d like to start working on a fix. Please let me know.
Hi @AchilleasKat! The issue is still open, go ahead!
Thanks for the confirmation! I’ll start working on it.