Add one dot to the end of a filename and some systems will still open the same file, while the security rule that guarded the original name no longer matches. That is CWE-42, path equivalence using a trailing dot, as in filedir..
MITRE describes it as a product accepting path input with a trailing dot without appropriate validation, leading to ambiguous path resolution. It is a child of the broader CWE-41, where one object has several names and the access control knows only some of them.
How it happens
Two components see the same string differently. A rule such as "files ending in .asp are executed, never shown" compares the file extension. The request page.asp. has a different last character, so the rule does not fire and the file is served as plain text. Then the file system layer drops the trailing dot and opens page.asp anyway.
The record lists four cases of source code disclosure using a trailing dot: CVE-2000-1114, CVE-2002-1986, CVE-2004-2213 and CVE-2005-3293. The dates tell you this has been around for a long time, and the 2004 entry also mentions a trailing encoded space, %20, which is the same trick with another character.
# Vulnerable: extension check happens on the raw name
def serve(name):
if name.endswith((".php", ".asp", ".jsp")):
return run_script(name)
return send_raw(name) # "page.asp." lands here
What an attacker gains
MITRE lists bypassing a protection mechanism under access control. The practical result is reading files that were meant to be executed or protected, which usually means source code, and with it hard coded secrets and logic flaws.
How to find it
Request each protected resource with one or more trailing dots in a test environment and compare responses to the plain name. Any difference in handling, particularly returned source or a skipped login, is a finding. Review code that makes decisions from an extension or a suffix.
How to fix it
- Canonicalize the path first, then apply the extension and access rules to the canonical result.
- Reject filenames with trailing dots or spaces unless there is a clear need for them.
- Handle files by allowlist: a known extension list that maps to handlers, and a default of deny.
- Keep source and configuration outside the web root.
import os
def serve(name):
clean = os.path.normpath(name)
if clean != name.rstrip(". ") or clean.startswith(".."):
return deny()
...
Servers and frameworks you did not write can carry the same bug, so keep them current. The free dependency scan at gold.safeguard.sh/scan needs no login.
Quick check
Why does a trailing dot bypass the rule?
The rule compares a string such as the extension, and the altered string does not match. A later layer normalizes the name and opens the original file.
What is the most reliable fix?
Canonicalize the path before any access or handler decision and decide on the canonical result.