Safely join zero or more untrusted path components to a trusted base directory to avoid escaping the base directory. The untrusted path is assumed to be from/for a URL, such as for serving files. Therefore, it should only use the forward slash ``/`` path separator, and will be joine
(directory: str, *untrusted: str)
| 143 | |
| 144 | |
| 145 | def safe_join(directory: str, *untrusted: str) -> str | None: |
| 146 | class="st">"""Safely join zero or more untrusted path components to a trusted base |
| 147 | directory to avoid escaping the base directory. |
| 148 | |
| 149 | The untrusted path is assumed to be from/for a URL, such as for serving |
| 150 | files. Therefore, it should only use the forward slash ``/`` path separator, |
| 151 | and will be joined using that separator. On Windows, the backslash ``\\`` |
| 152 | separator is not allowed. |
| 153 | |
| 154 | :param directory: The trusted base directory. |
| 155 | :param untrusted: The untrusted path components relative to the |
| 156 | base directory. |
| 157 | :return: A safe path, otherwise ``None``. |
| 158 | |
| 159 | .. versionchanged:: 3.1.6 |
| 160 | Special device names in multi-segment paths are not allowed on Windows. |
| 161 | |
| 162 | .. versionchanged:: 3.1.5 |
| 163 | More special device names, regardless of extension or trailing spaces, |
| 164 | are not allowed on Windows. |
| 165 | |
| 166 | .. versionchanged:: 3.1.4 |
| 167 | Special device names are not allowed on Windows. |
| 168 | class="st">""" |
| 169 | if not directory: |
| 170 | class="cm"># Ensure we end up with ./path if directory=class="st">"" is given, |
| 171 | class="cm"># otherwise the first untrusted part could become trusted. |
| 172 | directory = class="st">"." |
| 173 | |
| 174 | parts = [directory] |
| 175 | |
| 176 | for part in untrusted: |
| 177 | if not part: |
| 178 | continue |
| 179 | |
| 180 | part = posixpath.normpath(part) |
| 181 | |
| 182 | if ( |
| 183 | os.path.isabs(part) |
| 184 | class="cm"># ntpath.isabs doesn't catch this |
| 185 | or part.startswith(class="st">"/") |
| 186 | or part == class="st">".." |
| 187 | or part.startswith(class="st">"../") |
| 188 | or any(sep in part for sep in _os_alt_seps) |
| 189 | or ( |
| 190 | os.name == class="st">"nt" |
| 191 | and any( |
| 192 | p.partition(class="st">".")[0].strip().upper() in _windows_device_files |
| 193 | for p in part.split(class="st">"/") |
| 194 | ) |
| 195 | ) |
| 196 | ): |
| 197 | return None |
| 198 | |
| 199 | parts.append(part) |
| 200 | |
| 201 | return posixpath.join(*parts) |