Commit 91362c52 authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[webui] Add sanitized image data source

This data source allows embedding external images into WebUI pages via
the URL chrome://image?<image URL>.

No-try due to broken traffic annotation check. See crbug/1119509.

No-Try: true
Bug: 1087613
Change-Id: If7e6af0a90ce5032832eb3cc883999b5bc9ffb3e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2352517
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarLei Zhang <thestig@chromium.org>
Reviewed-by: default avatarNasko Oskov <nasko@chromium.org>
Reviewed-by: default avatarChristian Dullweber <dullweber@chromium.org>
Auto-Submit: Tibor Goldschwendt <tiborg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#799812}
parent c9dc8212
...@@ -1385,6 +1385,8 @@ static_library("ui") { ...@@ -1385,6 +1385,8 @@ static_library("ui") {
"webui/policy_indicator_localized_strings_provider.h", "webui/policy_indicator_localized_strings_provider.h",
"webui/profile_info_watcher.cc", "webui/profile_info_watcher.cc",
"webui/profile_info_watcher.h", "webui/profile_info_watcher.h",
"webui/sanitized_image_source.cc",
"webui/sanitized_image_source.h",
"webui/settings/about_handler.cc", "webui/settings/about_handler.cc",
"webui/settings/about_handler.h", "webui/settings/about_handler.h",
"webui/settings/accessibility_main_handler.cc", "webui/settings/accessibility_main_handler.cc",
......
// Copyright 2020 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 "chrome/browser/ui/webui/sanitized_image_source.h"
#include "base/memory/ref_counted_memory.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/strcat.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/image_fetcher/image_decoder_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/url_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "url/url_util.h"
SanitizedImageSource::SanitizedImageSource(Profile* profile)
: SanitizedImageSource(
profile,
content::BrowserContext::GetDefaultStoragePartition(profile)
->GetURLLoaderFactoryForBrowserProcess(),
std::make_unique<ImageDecoderImpl>()) {}
SanitizedImageSource::SanitizedImageSource(
Profile* profile,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<image_fetcher::ImageDecoder> image_decoder)
: url_loader_factory_(url_loader_factory),
image_decoder_(std::move(image_decoder)),
encode_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}
SanitizedImageSource::~SanitizedImageSource() = default;
std::string SanitizedImageSource::GetSource() {
return chrome::kChromeUIImageHost;
}
void SanitizedImageSource::StartDataRequest(
const GURL& url,
const content::WebContents::Getter& wc_getter,
content::URLDataSource::GotDataCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
GURL url_param = GURL(url.query());
if (!url_param.is_valid() ||
url != GURL(base::StrCat(
{chrome::kChromeUIImageURL, "?", url_param.spec()}))) {
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
return;
}
// Download the image body.
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("sanitized_image_source", R"(
semantics {
sender: "WebUI Sanitized Image Source"
description:
"This data source fetches an arbitrary image to be displayed in a "
"WebUI."
trigger:
"When a WebUI triggers the download of chrome://image?<URL> by "
"e.g. setting that URL as a src on an img tag."
data: "NONE"
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled by settings."
policy_exception_justification:
"This is a helper data source. It can be indirectly disabled by "
"disabling the requester WebUI."
})");
auto request = std::make_unique<network::ResourceRequest>();
request->url = url_param;
loaders_.push_back(
network::SimpleURLLoader::Create(std::move(request), traffic_annotation));
loaders_.back()->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&SanitizedImageSource::OnImageLoaded,
weak_ptr_factory_.GetWeakPtr(), loaders_.back().get(),
std::move(callback)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
std::string SanitizedImageSource::GetMimeType(const std::string& path) {
return "image/png";
}
void SanitizedImageSource::OnImageLoaded(
network::SimpleURLLoader* loader,
content::URLDataSource::GotDataCallback callback,
std::unique_ptr<std::string> body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Take loader out of |loaders_| list and store it in a unique_ptr on the
// stack to make sure the loader gets cleaned upon return.
auto it = std::find_if(
loaders_.begin(), loaders_.end(),
[loader](const std::unique_ptr<network::SimpleURLLoader>& target) {
return loader == target.get();
});
std::unique_ptr<network::SimpleURLLoader> loader_ptr(std::move(*it));
loaders_.erase(it);
if (loader->NetError() != net::OK || !body) {
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
return;
}
// Send image body to image decoder in isolated process.
image_decoder_->DecodeImage(
*body, gfx::Size() /* No particular size desired. */,
base::BindOnce(&SanitizedImageSource::OnImageDecoded,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void SanitizedImageSource::OnImageDecoded(
content::URLDataSource::GotDataCallback callback,
const gfx::Image& image) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Re-encode vetted image as PNG and send to requester.
encode_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](const SkBitmap& bitmap) {
auto encoded = base::MakeRefCounted<base::RefCountedBytes>();
return gfx::PNGCodec::EncodeBGRASkBitmap(
bitmap, /*discard_transparency=*/false, &encoded->data())
? encoded
: base::MakeRefCounted<base::RefCountedBytes>();
},
image.AsBitmap()),
std::move(callback));
}
// Copyright 2020 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.
#ifndef CHROME_BROWSER_UI_WEBUI_SANITIZED_IMAGE_SOURCE_H_
#define CHROME_BROWSER_UI_WEBUI_SANITIZED_IMAGE_SOURCE_H_
#include <list>
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "content/public/browser/url_data_source.h"
class Profile;
namespace base {
class SequencedTaskRunner;
} // namespace base
namespace gfx {
class Image;
} // namespace gfx
namespace image_fetcher {
class ImageDecoder;
} // namespace image_fetcher
namespace network {
class SharedURLLoaderFactory;
class SimpleURLLoader;
} // namespace network
// The sanitized image source provides a convenient mean to embed images into
// WebUIs. For security reasons WebUIs are not allowed to download and decode
// external images in their renderer process. The sanitized image source allows
// external images in WebUIs by downloading the image in the browser process,
// decoding the image in an isolated utility process, re-encoding the image as
// PNG and sending the now sanitized image back to the requesting WebUI. You can
// reach the image source via:
//
// chrome://image?<external image URL>
//
class SanitizedImageSource : public content::URLDataSource {
public:
explicit SanitizedImageSource(Profile* profile);
// This constructor lets us pass mock dependencies for testing.
SanitizedImageSource(
Profile* profile,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<image_fetcher::ImageDecoder> image_decoder);
SanitizedImageSource(const SanitizedImageSource&) = delete;
SanitizedImageSource& operator=(const SanitizedImageSource&) = delete;
~SanitizedImageSource() override;
// content::URLDataSource:
std::string GetSource() override;
void StartDataRequest(
const GURL& url,
const content::WebContents::Getter& wc_getter,
content::URLDataSource::GotDataCallback callback) override;
std::string GetMimeType(const std::string& path) override;
private:
void OnImageLoaded(network::SimpleURLLoader* loader,
content::URLDataSource::GotDataCallback callback,
std::unique_ptr<std::string> body);
void OnImageDecoded(content::URLDataSource::GotDataCallback callback,
const gfx::Image& image);
const scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
std::list<std::unique_ptr<network::SimpleURLLoader>> loaders_;
std::unique_ptr<image_fetcher::ImageDecoder> image_decoder_;
scoped_refptr<base::SequencedTaskRunner> encode_task_runner_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<SanitizedImageSource> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_WEBUI_SANITIZED_IMAGE_SOURCE_H_
// Copyright 2020 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 "chrome/browser/ui/webui/sanitized_image_source.h"
#include "base/strings/strcat.h"
#include "base/test/mock_callback.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/testing_profile.h"
#include "components/image_fetcher/core/image_decoder.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
namespace {
gfx::Image MakeImage(SkColor color) {
SkBitmap bitmap;
bitmap.allocN32Pixels(5, 5);
bitmap.eraseColor(color);
return gfx::Image::CreateFrom1xBitmap(bitmap);
}
} // namespace
MATCHER_P(MemoryEq, other, "Eq matcher for base::RefCountedMemory contents") {
return arg->Equals(other);
}
class MockImageDecoder : public image_fetcher::ImageDecoder {
public:
MOCK_METHOD3(DecodeImage,
void(const std::string&,
const gfx::Size&,
image_fetcher::ImageDecodedCallback));
};
class SanitizedImageSourceTest : public testing::Test {
public:
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
auto image_decoder = std::make_unique<MockImageDecoder>();
mock_image_decoder_ = image_decoder.get();
sanitized_image_source_ = std::make_unique<SanitizedImageSource>(
profile_.get(),
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_),
std::move(image_decoder));
}
void TearDown() override {
sanitized_image_source_.reset();
profile_.reset();
test_url_loader_factory_.ClearResponses();
}
protected:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
network::TestURLLoaderFactory test_url_loader_factory_;
MockImageDecoder* mock_image_decoder_;
std::unique_ptr<SanitizedImageSource> sanitized_image_source_;
};
// Verifies that the image source can handle multiple requests in parallel.
TEST_F(SanitizedImageSourceTest, MultiRequest) {
std::vector<std::tuple<SkColor, std::string, std::string>> data(
{{SK_ColorRED, "https://foo.com/img.png", "abc"},
{SK_ColorBLUE, "https://bar.com/img.png", "def"},
{SK_ColorGREEN, "https://baz.com/img.png", "ghi"}});
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
for (const auto& datum : data) {
SkColor color;
std::string url;
std::string body;
std::tie(color, url, body) = datum;
test_url_loader_factory_.AddResponse(url, body);
EXPECT_CALL(*mock_image_decoder_,
DecodeImage(body, gfx::Size(), testing::_))
.Times(1)
.WillOnce([color](const std::string&, const gfx::Size&,
image_fetcher::ImageDecodedCallback callback) {
std::move(callback).Run(MakeImage(color));
});
EXPECT_CALL(callback, Run(MemoryEq(MakeImage(color).As1xPNGBytes())))
.Times(1);
}
// Issue requests.
for (const auto& datum : data) {
std::string url;
std::tie(std::ignore, url, std::ignore) = datum;
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", url})),
content::WebContents::Getter(), callback.Get());
}
task_environment_.RunUntilIdle();
}
// Verifies that the image source sends back an empty image in case the external
// image download fails.
TEST_F(SanitizedImageSourceTest, FailedLoad) {
constexpr char kImageUrl[] = "https://foo.com/img.png";
// Set up expectations and mock data.
test_url_loader_factory_.AddResponse(kImageUrl, "", net::HTTP_NOT_FOUND);
EXPECT_CALL(*mock_image_decoder_,
DecodeImage(testing::_, testing::_, testing::_))
.Times(0);
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(callback,
Run(MemoryEq(base::MakeRefCounted<base::RefCountedString>())))
.Times(1);
// Issue request.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", kImageUrl})),
content::WebContents::Getter(), callback.Get());
task_environment_.RunUntilIdle();
}
// Verifies that the image source ignores requests with a wrong URL.
TEST_F(SanitizedImageSourceTest, WrongUrl) {
// Set up expectations and mock data.
EXPECT_CALL(*mock_image_decoder_,
DecodeImage(testing::_, testing::_, testing::_))
.Times(0);
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(callback,
Run(MemoryEq(base::MakeRefCounted<base::RefCountedString>())))
.Times(2);
// Issue request.
sanitized_image_source_->StartDataRequest(
GURL("chrome://abc?https://foo.com/img.png"),
content::WebContents::Getter(), callback.Get());
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?abc"})),
content::WebContents::Getter(), callback.Get());
task_environment_.RunUntilIdle();
EXPECT_EQ(0, test_url_loader_factory_.NumPending());
}
...@@ -88,6 +88,8 @@ const char kChromeUIHistoryHost[] = "history"; ...@@ -88,6 +88,8 @@ const char kChromeUIHistoryHost[] = "history";
const char kChromeUIHistorySyncedTabs[] = "/syncedTabs"; const char kChromeUIHistorySyncedTabs[] = "/syncedTabs";
const char kChromeUIHistoryURL[] = "chrome://history/"; const char kChromeUIHistoryURL[] = "chrome://history/";
const char kChromeUIIdentityInternalsHost[] = "identity-internals"; const char kChromeUIIdentityInternalsHost[] = "identity-internals";
const char kChromeUIImageHost[] = "image";
const char kChromeUIImageURL[] = "chrome://image/";
const char kChromeUIInspectHost[] = "inspect"; const char kChromeUIInspectHost[] = "inspect";
const char kChromeUIInspectURL[] = "chrome://inspect/"; const char kChromeUIInspectURL[] = "chrome://inspect/";
const char kChromeUIInternalsHost[] = "internals"; const char kChromeUIInternalsHost[] = "internals";
......
...@@ -91,6 +91,8 @@ extern const char kChromeUIHistoryHost[]; ...@@ -91,6 +91,8 @@ extern const char kChromeUIHistoryHost[];
extern const char kChromeUIHistorySyncedTabs[]; extern const char kChromeUIHistorySyncedTabs[];
extern const char kChromeUIHistoryURL[]; extern const char kChromeUIHistoryURL[];
extern const char kChromeUIIdentityInternalsHost[]; extern const char kChromeUIIdentityInternalsHost[];
extern const char kChromeUIImageHost[];
extern const char kChromeUIImageURL[];
extern const char kChromeUIInspectHost[]; extern const char kChromeUIInspectHost[];
extern const char kChromeUIInspectURL[]; extern const char kChromeUIInspectURL[];
extern const char kChromeUIInternalsHost[]; extern const char kChromeUIInternalsHost[];
......
...@@ -4367,6 +4367,7 @@ test("unit_tests") { ...@@ -4367,6 +4367,7 @@ test("unit_tests") {
"../browser/ui/webui/management_ui_handler_unittest.cc", "../browser/ui/webui/management_ui_handler_unittest.cc",
"../browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc", "../browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc",
"../browser/ui/webui/new_tab_page/promo_browser_command/promo_browser_command_handler_unittest.cc", "../browser/ui/webui/new_tab_page/promo_browser_command/promo_browser_command_handler_unittest.cc",
"../browser/ui/webui/sanitized_image_source_unittest.cc",
"../browser/ui/webui/settings/downloads_handler_unittest.cc", "../browser/ui/webui/settings/downloads_handler_unittest.cc",
"../browser/ui/webui/settings/hats_handler_unittest.cc", "../browser/ui/webui/settings/hats_handler_unittest.cc",
"../browser/ui/webui/settings/metrics_reporting_handler_unittest.cc", "../browser/ui/webui/settings/metrics_reporting_handler_unittest.cc",
......
...@@ -291,6 +291,7 @@ Refer to README.md for content description and update process. ...@@ -291,6 +291,7 @@ Refer to README.md for content description and update process.
<item id="safe_browsing_v4_get_hash" hash_code="8561691" type="0" content_hash_code="132435617" os_list="linux,windows" file_path="components/safe_browsing/core/db/v4_get_hash_protocol_manager.cc"/> <item id="safe_browsing_v4_get_hash" hash_code="8561691" type="0" content_hash_code="132435617" os_list="linux,windows" file_path="components/safe_browsing/core/db/v4_get_hash_protocol_manager.cc"/>
<item id="safe_browsing_v4_update" hash_code="82509217" type="0" content_hash_code="5247849" os_list="linux,windows" file_path="components/safe_browsing/core/db/v4_update_protocol_manager.cc"/> <item id="safe_browsing_v4_update" hash_code="82509217" type="0" content_hash_code="5247849" os_list="linux,windows" file_path="components/safe_browsing/core/db/v4_update_protocol_manager.cc"/>
<item id="safety_check_update_connectivity" hash_code="137724067" type="0" content_hash_code="30977369" os_list="linux,windows" file_path="components/safety_check/update_check_helper.cc"/> <item id="safety_check_update_connectivity" hash_code="137724067" type="0" content_hash_code="30977369" os_list="linux,windows" file_path="components/safety_check/update_check_helper.cc"/>
<item id="sanitized_image_source" hash_code="82344448" type="0" content_hash_code="39770427" os_list="linux,windows" file_path="chrome/browser/ui/webui/sanitized_image_source.cc"/>
<item id="save_file_manager" hash_code="56275203" type="0" content_hash_code="56692339" os_list="linux,windows" file_path="content/browser/download/save_file_manager.cc"/> <item id="save_file_manager" hash_code="56275203" type="0" content_hash_code="56692339" os_list="linux,windows" file_path="content/browser/download/save_file_manager.cc"/>
<item id="sdch_dictionary_fetch" hash_code="47152935" type="0" deprecated="2017-09-16" content_hash_code="16764294" file_path=""/> <item id="sdch_dictionary_fetch" hash_code="47152935" type="0" deprecated="2017-09-16" content_hash_code="16764294" file_path=""/>
<item id="search_suggest_service" hash_code="57785193" type="0" content_hash_code="132247652" os_list="linux,windows" file_path="chrome/browser/search/search_suggest/search_suggest_loader_impl.cc"/> <item id="search_suggest_service" hash_code="57785193" type="0" content_hash_code="132247652" os_list="linux,windows" file_path="chrome/browser/search/search_suggest/search_suggest_loader_impl.cc"/>
......
...@@ -133,6 +133,7 @@ hidden="true" so that these annotations don't show up in the document. ...@@ -133,6 +133,7 @@ hidden="true" so that these annotations don't show up in the document.
<traffic_annotation unique_id="floc_id_provider_impl"/> <traffic_annotation unique_id="floc_id_provider_impl"/>
<traffic_annotation unique_id="gstatic_change_password_override_urls"/> <traffic_annotation unique_id="gstatic_change_password_override_urls"/>
<traffic_annotation unique_id="interest_feedv2_image_send"/> <traffic_annotation unique_id="interest_feedv2_image_send"/>
<traffic_annotation unique_id="sanitized_image_source"/>
</sender> </sender>
</group> </group>
<group name="Admin Features"> <group name="Admin Features">
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment