filename_util_internal.cc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. // Copyright 2014 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "net/base/filename_util.h"
  5. #include "base/containers/contains.h"
  6. #include "base/files/file_path.h"
  7. #include "base/files/file_util.h"
  8. #include "base/strings/escape.h"
  9. #include "base/strings/string_util.h"
  10. #include "base/strings/sys_string_conversions.h"
  11. #include "base/strings/utf_string_conversions.h"
  12. #include "base/threading/thread_restrictions.h"
  13. #include "build/build_config.h"
  14. #include "net/base/filename_util_internal.h"
  15. #include "net/base/mime_util.h"
  16. #include "net/base/net_string_util.h"
  17. #include "net/http/http_content_disposition.h"
  18. #include "url/gurl.h"
  19. namespace net {
  20. namespace {
  21. // Examines the current extension in |file_name| and tries to return the correct
  22. // extension the file should actually be using. Used by EnsureSafeExtension.
  23. // All other code should use EnsureSafeExtension, as it includes additional
  24. // safety checks.
  25. base::FilePath::StringType GetCorrectedExtensionUnsafe(
  26. const std::string& mime_type,
  27. bool ignore_extension,
  28. const base::FilePath& file_name) {
  29. // See if the file name already contains an extension.
  30. base::FilePath::StringType extension = file_name.Extension();
  31. if (!extension.empty())
  32. extension.erase(extension.begin()); // Erase preceding '.'.
  33. // Nothing to do if there's no mime type.
  34. if (mime_type.empty())
  35. return extension;
  36. // Nothing to do there's an extension, unless |ignore_extension| is true.
  37. if (!extension.empty() && !ignore_extension)
  38. return extension;
  39. // Don't do anything if there's not a preferred extension for the mime
  40. // type.
  41. base::FilePath::StringType preferred_mime_extension;
  42. if (!GetPreferredExtensionForMimeType(mime_type, &preferred_mime_extension))
  43. return extension;
  44. // If the existing extension is in the list of valid extensions for the
  45. // given type, use it. This avoids doing things like pointlessly renaming
  46. // "foo.jpg" to "foo.jpeg".
  47. std::vector<base::FilePath::StringType> all_mime_extensions;
  48. GetExtensionsForMimeType(mime_type, &all_mime_extensions);
  49. if (base::Contains(all_mime_extensions, extension))
  50. return extension;
  51. // Get the "final" extension. In most cases, this is the same as the
  52. // |extension|, but in cases like "foo.tar.gz", it's "gz" while
  53. // |extension| is "tar.gz".
  54. base::FilePath::StringType final_extension = file_name.FinalExtension();
  55. // Erase preceding '.'.
  56. if (!final_extension.empty())
  57. final_extension.erase(final_extension.begin());
  58. // If there's a double extension, and the second extension is in the
  59. // list of valid extensions for the given type, keep the double extension.
  60. // This avoids renaming things like "foo.tar.gz" to "foo.gz".
  61. if (base::Contains(all_mime_extensions, final_extension))
  62. return extension;
  63. return preferred_mime_extension;
  64. }
  65. } // namespace
  66. void SanitizeGeneratedFileName(base::FilePath::StringType* filename,
  67. bool replace_trailing) {
  68. const base::FilePath::CharType kReplace[] = FILE_PATH_LITERAL("_");
  69. if (filename->empty())
  70. return;
  71. if (replace_trailing) {
  72. // Handle CreateFile() stripping trailing dots and spaces on filenames
  73. // http://support.microsoft.com/kb/115827
  74. size_t length = filename->size();
  75. size_t pos = filename->find_last_not_of(FILE_PATH_LITERAL(" ."));
  76. filename->resize((pos == std::string::npos) ? 0 : (pos + 1));
  77. #if BUILDFLAG(IS_WIN)
  78. base::TrimWhitespace(*filename, base::TRIM_TRAILING, filename);
  79. #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
  80. base::TrimWhitespaceASCII(*filename, base::TRIM_TRAILING, filename);
  81. #else
  82. #error Unsupported platform
  83. #endif
  84. if (filename->empty())
  85. return;
  86. size_t trimmed = length - filename->size();
  87. if (trimmed)
  88. filename->insert(filename->end(), trimmed, kReplace[0]);
  89. }
  90. base::TrimString(*filename, FILE_PATH_LITERAL("."), filename);
  91. if (filename->empty())
  92. return;
  93. // Replace any path information by changing path separators.
  94. base::ReplaceSubstringsAfterOffset(
  95. filename, 0, FILE_PATH_LITERAL("/"), kReplace);
  96. base::ReplaceSubstringsAfterOffset(
  97. filename, 0, FILE_PATH_LITERAL("\\"), kReplace);
  98. }
  99. // Returns the filename determined from the last component of the path portion
  100. // of the URL. Returns an empty string if the URL doesn't have a path or is
  101. // invalid. If the generated filename is not reliable,
  102. // |should_overwrite_extension| will be set to true, in which case a better
  103. // extension should be determined based on the content type.
  104. std::string GetFileNameFromURL(const GURL& url,
  105. const std::string& referrer_charset,
  106. bool* should_overwrite_extension) {
  107. // about: and data: URLs don't have file names, but esp. data: URLs may
  108. // contain parts that look like ones (i.e., contain a slash). Therefore we
  109. // don't attempt to divine a file name out of them.
  110. if (!url.is_valid() || url.SchemeIs("about") || url.SchemeIs("data"))
  111. return std::string();
  112. std::string unescaped_url_filename = base::UnescapeBinaryURLComponent(
  113. url.ExtractFileName(), base::UnescapeRule::NORMAL);
  114. // The URL's path should be escaped UTF-8, but may not be.
  115. std::string decoded_filename = unescaped_url_filename;
  116. if (!base::IsStringUTF8(decoded_filename)) {
  117. // TODO(jshin): this is probably not robust enough. To be sure, we need
  118. // encoding detection.
  119. std::u16string utf16_output;
  120. if (!referrer_charset.empty() &&
  121. ConvertToUTF16(unescaped_url_filename, referrer_charset.c_str(),
  122. &utf16_output)) {
  123. decoded_filename = base::UTF16ToUTF8(utf16_output);
  124. } else {
  125. decoded_filename =
  126. base::WideToUTF8(base::SysNativeMBToWide(unescaped_url_filename));
  127. }
  128. }
  129. // If the URL contains a (possibly empty) query, assume it is a generator, and
  130. // allow the determined extension to be overwritten.
  131. *should_overwrite_extension = !decoded_filename.empty() && url.has_query();
  132. return decoded_filename;
  133. }
  134. // Returns whether the specified extension is automatically integrated into the
  135. // windows shell.
  136. bool IsShellIntegratedExtension(const base::FilePath::StringType& extension) {
  137. base::FilePath::StringType extension_lower = base::ToLowerASCII(extension);
  138. // .lnk files may be used to execute arbitrary code (see
  139. // https://nvd.nist.gov/vuln/detail/CVE-2010-2568). .local files are used by
  140. // Windows to determine which DLLs to load for an application.
  141. if ((extension_lower == FILE_PATH_LITERAL("local")) ||
  142. (extension_lower == FILE_PATH_LITERAL("lnk")))
  143. return true;
  144. // Setting a file's extension to a CLSID may conceal its actual file type on
  145. // some Windows versions (see https://nvd.nist.gov/vuln/detail/CVE-2004-0420).
  146. if (!extension_lower.empty() &&
  147. (extension_lower.front() == FILE_PATH_LITERAL('{')) &&
  148. (extension_lower.back() == FILE_PATH_LITERAL('}')))
  149. return true;
  150. return false;
  151. }
  152. // Examines the current extension in |file_name| and modifies it if necessary in
  153. // order to ensure the filename is safe. If |file_name| doesn't contain an
  154. // extension or if |ignore_extension| is true, then a new extension will be
  155. // constructed based on the |mime_type|.
  156. //
  157. // We're addressing two things here:
  158. //
  159. // 1) Usability. If there is no reliable file extension, we want to guess a
  160. // reasonable file extension based on the content type.
  161. //
  162. // 2) Shell integration. Some file extensions automatically integrate with the
  163. // shell. We block these extensions to prevent a malicious web site from
  164. // integrating with the user's shell.
  165. void EnsureSafeExtension(const std::string& mime_type,
  166. bool ignore_extension,
  167. base::FilePath* file_name) {
  168. DCHECK(file_name);
  169. base::FilePath::StringType extension =
  170. GetCorrectedExtensionUnsafe(mime_type, ignore_extension, *file_name);
  171. #if BUILDFLAG(IS_WIN)
  172. const base::FilePath::CharType kDefaultExtension[] =
  173. FILE_PATH_LITERAL("download");
  174. // Rename shell-integrated extensions.
  175. // TODO(asanka): Consider stripping out the bad extension and replacing it
  176. // with the preferred extension for the MIME type if one is available.
  177. if (IsShellIntegratedExtension(extension))
  178. extension = kDefaultExtension;
  179. #endif
  180. *file_name = file_name->ReplaceExtension(extension);
  181. }
  182. bool FilePathToString16(const base::FilePath& path, std::u16string* converted) {
  183. #if BUILDFLAG(IS_WIN)
  184. converted->assign(path.value().begin(), path.value().end());
  185. return true;
  186. #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
  187. std::string component8 = path.AsUTF8Unsafe();
  188. return !component8.empty() &&
  189. base::UTF8ToUTF16(component8.c_str(), component8.size(), converted);
  190. #endif
  191. }
  192. std::u16string GetSuggestedFilenameImpl(
  193. const GURL& url,
  194. const std::string& content_disposition,
  195. const std::string& referrer_charset,
  196. const std::string& suggested_name,
  197. const std::string& mime_type,
  198. const std::string& default_name,
  199. bool should_replace_extension,
  200. ReplaceIllegalCharactersFunction replace_illegal_characters_function) {
  201. // TODO: this function to be updated to match the httpbis recommendations.
  202. // Talk to abarth for the latest news.
  203. // We don't translate this fallback string, "download". If localization is
  204. // needed, the caller should provide localized fallback in |default_name|.
  205. static const base::FilePath::CharType kFinalFallbackName[] =
  206. FILE_PATH_LITERAL("download");
  207. std::string filename; // In UTF-8
  208. bool overwrite_extension = false;
  209. bool is_name_from_content_disposition = false;
  210. // Try to extract a filename from content-disposition first.
  211. if (!content_disposition.empty()) {
  212. HttpContentDisposition header(content_disposition, referrer_charset);
  213. filename = header.filename();
  214. if (!filename.empty())
  215. is_name_from_content_disposition = true;
  216. }
  217. // Then try to use the suggested name.
  218. if (filename.empty() && !suggested_name.empty())
  219. filename = suggested_name;
  220. // Now try extracting the filename from the URL. GetFileNameFromURL() only
  221. // looks at the last component of the URL and doesn't return the hostname as a
  222. // failover.
  223. if (filename.empty())
  224. filename = GetFileNameFromURL(url, referrer_charset, &overwrite_extension);
  225. // Finally try the URL hostname, but only if there's no default specified in
  226. // |default_name|. Some schemes (e.g.: file:, about:, data:) do not have a
  227. // host name.
  228. if (filename.empty() && default_name.empty() && url.is_valid() &&
  229. !url.host().empty()) {
  230. // TODO(jungshik) : Decode a 'punycoded' IDN hostname. (bug 1264451)
  231. filename = url.host();
  232. }
  233. bool replace_trailing = false;
  234. base::FilePath::StringType result_str, default_name_str;
  235. #if BUILDFLAG(IS_WIN)
  236. replace_trailing = true;
  237. result_str = base::UTF8ToWide(filename);
  238. default_name_str = base::UTF8ToWide(default_name);
  239. #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
  240. result_str = filename;
  241. default_name_str = default_name;
  242. #else
  243. #error Unsupported platform
  244. #endif
  245. SanitizeGeneratedFileName(&result_str, replace_trailing);
  246. if (result_str.find_last_not_of(FILE_PATH_LITERAL("-_")) ==
  247. base::FilePath::StringType::npos) {
  248. result_str = !default_name_str.empty()
  249. ? default_name_str
  250. : base::FilePath::StringType(kFinalFallbackName);
  251. overwrite_extension = false;
  252. }
  253. replace_illegal_characters_function(&result_str, '_');
  254. base::FilePath result(result_str);
  255. overwrite_extension |= should_replace_extension;
  256. // extension should not appended to filename derived from
  257. // content-disposition, if it does not have one.
  258. // Hence mimetype and overwrite_extension values are not used.
  259. if (is_name_from_content_disposition)
  260. GenerateSafeFileName("", false, &result);
  261. else
  262. GenerateSafeFileName(mime_type, overwrite_extension, &result);
  263. std::u16string result16;
  264. if (!FilePathToString16(result, &result16)) {
  265. result = base::FilePath(default_name_str);
  266. if (!FilePathToString16(result, &result16)) {
  267. result = base::FilePath(kFinalFallbackName);
  268. FilePathToString16(result, &result16);
  269. }
  270. }
  271. return result16;
  272. }
  273. base::FilePath GenerateFileNameImpl(
  274. const GURL& url,
  275. const std::string& content_disposition,
  276. const std::string& referrer_charset,
  277. const std::string& suggested_name,
  278. const std::string& mime_type,
  279. const std::string& default_file_name,
  280. bool should_replace_extension,
  281. ReplaceIllegalCharactersFunction replace_illegal_characters_function) {
  282. std::u16string file_name = GetSuggestedFilenameImpl(
  283. url, content_disposition, referrer_charset, suggested_name, mime_type,
  284. default_file_name, should_replace_extension,
  285. replace_illegal_characters_function);
  286. #if BUILDFLAG(IS_WIN)
  287. base::FilePath generated_name(base::AsWStringPiece(file_name));
  288. #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
  289. base::FilePath generated_name(
  290. base::SysWideToNativeMB(base::UTF16ToWide(file_name)));
  291. #endif
  292. DCHECK(!generated_name.empty());
  293. return generated_name;
  294. }
  295. } // namespace net