| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/download/quarantine/quarantine.h" |
| |
| #include <windows.h> |
| #include <wrl/client.h> |
| |
| #include <cguid.h> |
| #include <objbase.h> |
| #include <shellapi.h> |
| #include <shlobj.h> |
| #include <shobjidl.h> |
| #include <wininet.h> |
| |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/guid.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/win/scoped_handle.h" |
| #include "url/gurl.h" |
| |
| namespace download { |
| namespace { |
| |
| // [MS-FSCC] Section 5.6.1 |
| const base::FilePath::CharType kZoneIdentifierStreamSuffix[] = |
| FILE_PATH_LITERAL(":Zone.Identifier"); |
| |
| bool ZoneIdentifierPresentForFile(const base::FilePath& path) { |
| const DWORD kShare = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; |
| base::FilePath::StringType zone_identifier_path = |
| path.value() + kZoneIdentifierStreamSuffix; |
| base::win::ScopedHandle file( |
| CreateFile(zone_identifier_path.c_str(), GENERIC_READ, kShare, nullptr, |
| OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); |
| if (!file.IsValid()) |
| return false; |
| |
| // The zone identifier contents is expected to be: |
| // "[ZoneTransfer]\r\nZoneId=3\r\n". The actual ZoneId can be different. A |
| // buffer of 32 bytes is sufficient for verifying the contents. |
| std::vector<char> zone_identifier_contents_buffer(32); |
| DWORD actual_length = 0; |
| if (!ReadFile(file.Get(), &zone_identifier_contents_buffer.front(), |
| zone_identifier_contents_buffer.size(), &actual_length, |
| nullptr)) |
| return false; |
| zone_identifier_contents_buffer.resize(actual_length); |
| |
| std::string zone_identifier_contents(zone_identifier_contents_buffer.begin(), |
| zone_identifier_contents_buffer.end()); |
| |
| std::vector<base::StringPiece> lines = |
| base::SplitStringPiece(zone_identifier_contents, "\n", |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| return lines.size() > 1 && lines[0] == "[ZoneTransfer]" && |
| lines[1].find("ZoneId=") == 0; |
| } |
| |
| // Sets the Zone Identifier on the file to "Internet" (3). Returns true if the |
| // function succeeds, false otherwise. A failure is expected if alternate |
| // streams are not supported, like a file on a FAT32 filesystem. This function |
| // does not invoke Windows Attachment Execution Services. |
| // |
| // |full_path| is the path to the downloaded file. |
| QuarantineFileResult SetInternetZoneIdentifierDirectly( |
| const base::FilePath& full_path) { |
| const DWORD kShare = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; |
| std::wstring path = full_path.value() + kZoneIdentifierStreamSuffix; |
| HANDLE file = CreateFile(path.c_str(), GENERIC_WRITE, kShare, nullptr, |
| OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); |
| if (INVALID_HANDLE_VALUE == file) |
| return QuarantineFileResult::ANNOTATION_FAILED; |
| |
| static const char kIdentifier[] = "[ZoneTransfer]\r\nZoneId=3\r\n"; |
| // Don't include trailing null in data written. |
| static const DWORD kIdentifierSize = arraysize(kIdentifier) - 1; |
| DWORD written = 0; |
| BOOL write_result = |
| WriteFile(file, kIdentifier, kIdentifierSize, &written, nullptr); |
| BOOL flush_result = FlushFileBuffers(file); |
| CloseHandle(file); |
| |
| return write_result && flush_result && written == kIdentifierSize |
| ? QuarantineFileResult::OK |
| : QuarantineFileResult::ANNOTATION_FAILED; |
| } |
| |
| // Invokes IAttachmentExecute::Save on CLSID_AttachmentServices to validate the |
| // downloaded file. The call may scan the file for viruses and if necessary, |
| // annotate it with evidence. As a result of the validation, the file may be |
| // deleted. See: http://msdn.microsoft.com/en-us/bb776299 |
| // |
| // IAE::Save() will delete the file if it was found to be blocked by local |
| // security policy or if it was found to be infected. The call may also delete |
| // the file due to other failures (http://crbug.com/153212). A failure code will |
| // be returned in these cases. |
| // |
| // The return value is |false| iff the function fails to invoke |
| // IAttachmentExecute::Save(). If the function returns |true|, then the result |
| // of invoking IAttachmentExecute::Save() is stored in |save_result|. |
| // |
| // Typical |save_result| values: |
| // S_OK : The file was okay. If any viruses were found, they were cleaned. |
| // E_FAIL : Virus infected. |
| // INET_E_SECURITY_PROBLEM : The file was blocked due to security policy. |
| // |
| // Any other return value indicates an unexpected error during the scan. |
| // |
| // |full_path| : is the path to the downloaded file. This should be the final |
| // path of the download. Must be present. |
| // |source_url|: the source URL for the download. If empty, the source will |
| // be set to 'about:internet'. |
| // |referrer_url|: the referrer URL for the download. If empty, the referrer |
| // will not be set. |
| // |client_guid|: the GUID to be set in the IAttachmentExecute client slot. |
| // Used to identify the app to the system AV function. |
| // |save_result|: Receives the result of invoking IAttachmentExecute::Save(). |
| bool InvokeAttachmentServices(const base::FilePath& full_path, |
| const std::string& source_url, |
| const std::string& referrer_url, |
| const GUID& client_guid, |
| HRESULT* save_result) { |
| Microsoft::WRL::ComPtr<IAttachmentExecute> attachment_services; |
| HRESULT hr = ::CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL, |
| IID_PPV_ARGS(&attachment_services)); |
| *save_result = S_OK; |
| |
| if (FAILED(hr)) { |
| // The thread must have COM initialized. |
| DCHECK_NE(CO_E_NOTINITIALIZED, hr); |
| return false; |
| } |
| |
| // Note that it is mandatory to check the return values from here on out. If |
| // setting one of the parameters fails, it could leave the object in a state |
| // where the final Save() call will also fail. |
| |
| hr = attachment_services->SetClientGuid(client_guid); |
| if (FAILED(hr)) |
| return false; |
| |
| hr = attachment_services->SetLocalPath(full_path.value().c_str()); |
| if (FAILED(hr)) |
| return false; |
| |
| // The source URL could be empty if it was not a valid URL, or was not HTTP/S, |
| // or the download was off-the-record. If so, user "about:internet" as a |
| // fallback URL. The latter is known to reliably map to the Internet zone. |
| // |
| // In addition, URLs that are longer than INTERNET_MAX_URL_LENGTH are also |
| // known to cause problems for URLMon. Hence also use "about:internet" in |
| // these cases. See http://crbug.com/601538. |
| hr = attachment_services->SetSource( |
| source_url.empty() || source_url.size() > INTERNET_MAX_URL_LENGTH |
| ? L"about:internet" |
| : base::UTF8ToWide(source_url).c_str()); |
| if (FAILED(hr)) |
| return false; |
| |
| // Only set referrer if one is present and shorter than |
| // INTERNET_MAX_URL_LENGTH. Also, the source_url is authoritative for |
| // determining the relative danger of |full_path| so we don't consider it an |
| // error if we have to skip the |referrer_url|. |
| if (!referrer_url.empty() && referrer_url.size() < INTERNET_MAX_URL_LENGTH) { |
| hr = attachment_services->SetReferrer( |
| base::UTF8ToWide(referrer_url).c_str()); |
| if (FAILED(hr)) |
| return false; |
| } |
| |
| { |
| // This method has been known to take longer than 10 seconds in some |
| // instances. |
| SCOPED_UMA_HISTOGRAM_LONG_TIMER("Download.AttachmentServices.Duration"); |
| *save_result = attachment_services->Save(); |
| } |
| return true; |
| } |
| |
| // Maps a return code from an unsuccessful IAttachmentExecute::Save() call to a |
| // QuarantineFileResult. |
| QuarantineFileResult FailedSaveResultToQuarantineResult(HRESULT result) { |
| switch (result) { |
| case INET_E_SECURITY_PROBLEM: // 0x800c000e |
| // This is returned if the download was blocked due to security |
| // restrictions. E.g. if the source URL was in the Restricted Sites zone |
| // and downloads are blocked on that zone, then the download would be |
| // deleted and this error code is returned. |
| return QuarantineFileResult::BLOCKED_BY_POLICY; |
| |
| case E_FAIL: // 0x80004005 |
| // Returned if an anti-virus product reports an infection in the |
| // downloaded file during IAE::Save(). |
| return QuarantineFileResult::VIRUS_INFECTED; |
| |
| default: |
| // Any other error that occurs during IAttachmentExecute::Save() likely |
| // indicates a problem with the security check, but not necessarily the |
| // download. This also includes cases where SUCCEEDED(result) is true. In |
| // the latter case we are likely dealing with a situation where the file |
| // is missing after a successful scan. See http://crbug.com/153212. |
| return QuarantineFileResult::SECURITY_CHECK_FAILED; |
| } |
| } |
| |
| } // namespace |
| |
| QuarantineFileResult QuarantineFile(const base::FilePath& file, |
| const GURL& source_url, |
| const GURL& referrer_url, |
| const std::string& client_guid) { |
| base::AssertBlockingAllowed(); |
| |
| int64_t file_size = 0; |
| if (!base::PathExists(file) || !base::GetFileSize(file, &file_size)) |
| return QuarantineFileResult::FILE_MISSING; |
| |
| std::string braces_guid = "{" + client_guid + "}"; |
| GUID guid = GUID_NULL; |
| if (base::IsValidGUID(client_guid)) { |
| HRESULT hr = CLSIDFromString(base::UTF8ToUTF16(braces_guid).c_str(), &guid); |
| if (FAILED(hr)) |
| guid = GUID_NULL; |
| } |
| |
| if (file_size == 0 || IsEqualGUID(guid, GUID_NULL)) { |
| // Calling InvokeAttachmentServices on an empty file can result in the file |
| // being deleted. Also an anti-virus scan doesn't make a lot of sense to |
| // perform on an empty file. |
| return SetInternetZoneIdentifierDirectly(file); |
| } |
| |
| HRESULT save_result = S_OK; |
| bool attachment_services_available = InvokeAttachmentServices( |
| file, source_url.spec(), referrer_url.spec(), guid, &save_result); |
| if (!attachment_services_available) |
| return SetInternetZoneIdentifierDirectly(file); |
| |
| // If the download file is missing after the call, then treat this as an |
| // interrupted download. |
| // |
| // If InvokeAttachmentServices() failed, but the downloaded file is still |
| // around, then don't interrupt the download. Attachment Execution Services |
| // deletes the submitted file if the downloaded file is blocked by policy or |
| // if it was found to be infected. |
| // |
| // If the file is still there, then the error could be due to Windows |
| // Attachment Services not being available or some other error during the AES |
| // invocation. In either case, we don't surface the error to the user. |
| if (!base::PathExists(file)) |
| return FailedSaveResultToQuarantineResult(save_result); |
| return QuarantineFileResult::OK; |
| } |
| |
| bool IsFileQuarantined(const base::FilePath& file, |
| const GURL& source_url, |
| const GURL& referrer_url) { |
| return ZoneIdentifierPresentForFile(file); |
| } |
| |
| } // namespace download |