Commit b39dbbb2 authored by Olivier Yiptong's avatar Olivier Yiptong Committed by Commit Bot

FontAccess: User Activation Checks

The exposed data in Font Access can be used for fingerprinting.

Implements user activation checks for the Font Access API as means to
further gate this API, which has a permission request flow already.

The permission prompt and UA checks together greatly mitigate
accidental or malicious activations of this API.

A blink-side UA check first gates access to the API. But since we ought
to trust the browser process rather than a renderer process, a check is
also made browser-side.

Fixes: 1116194
Change-Id: I1e95433fe7139489adeb940aed1662a3fe645bc5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2359236
Commit-Queue: Olivier Yiptong <oyiptong@chromium.org>
Reviewed-by: default avatarJoshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#798988}
parent 90123183
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "content/browser/permissions/permission_controller_impl.h" #include "content/browser/permissions/permission_controller_impl.h"
#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h" #include "content/public/browser/render_process_host.h"
#include "third_party/blink/public/common/features.h"
namespace content { namespace content {
...@@ -20,6 +21,7 @@ FontAccessManagerImpl::~FontAccessManagerImpl() { ...@@ -20,6 +21,7 @@ FontAccessManagerImpl::~FontAccessManagerImpl() {
void FontAccessManagerImpl::BindReceiver( void FontAccessManagerImpl::BindReceiver(
const BindingContext& context, const BindingContext& context,
mojo::PendingReceiver<blink::mojom::FontAccessManager> receiver) { mojo::PendingReceiver<blink::mojom::FontAccessManager> receiver) {
DCHECK(base::FeatureList::IsEnabled(blink::features::kFontAccess));
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
receivers_.Add(this, std::move(receiver), context); receivers_.Add(this, std::move(receiver), context);
...@@ -32,11 +34,19 @@ void FontAccessManagerImpl::RequestPermission( ...@@ -32,11 +34,19 @@ void FontAccessManagerImpl::RequestPermission(
const BindingContext& context = receivers_.current_context(); const BindingContext& context = receivers_.current_context();
RenderFrameHost* rfh = RenderFrameHost::FromID(context.frame_id); RenderFrameHost* rfh = RenderFrameHost::FromID(context.frame_id);
// Double checking: renderer processes should already have checked for user
// activation before the RPC has been made. It is not an error, because it is
// possible that user activation has lapsed before reaching here.
if (!rfh->HasTransientUserActivation()) {
std::move(callback).Run(blink::mojom::PermissionStatus::DENIED);
return;
}
auto* permission_controller = PermissionControllerImpl::FromBrowserContext( auto* permission_controller = PermissionControllerImpl::FromBrowserContext(
rfh->GetProcess()->GetBrowserContext()); rfh->GetProcess()->GetBrowserContext());
permission_controller->RequestPermission( permission_controller->RequestPermission(
PermissionType::FONT_ACCESS, rfh, context.origin.GetURL(), PermissionType::FONT_ACCESS, rfh, context.origin.GetURL(),
/*user_gesture=*/false, /*user_gesture=*/true,
base::BindOnce( base::BindOnce(
[](RequestPermissionCallback callback, [](RequestPermissionCallback callback,
blink::mojom::PermissionStatus status) { blink::mojom::PermissionStatus status) {
......
// 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 "content/browser/font_access/font_access_manager_impl.h"
#include "base/run_loop.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/public/test/mock_permission_manager.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/test_render_frame_host.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using PermissionCallback =
base::OnceCallback<void(blink::mojom::PermissionStatus)>;
class TestPermissionManager : public MockPermissionManager {
public:
TestPermissionManager() = default;
~TestPermissionManager() override = default;
int RequestPermission(PermissionType permissions,
RenderFrameHost* render_frame_host,
const GURL& requesting_origin,
bool user_gesture,
PermissionCallback callback) override {
EXPECT_EQ(permissions, PermissionType::FONT_ACCESS);
EXPECT_TRUE(user_gesture);
request_callback_.Run(std::move(callback));
return 0;
}
void SetRequestCallback(
base::RepeatingCallback<void(PermissionCallback)> request_callback) {
request_callback_ = std::move(request_callback);
}
private:
base::RepeatingCallback<void(PermissionCallback)> request_callback_;
};
} // namespace
class FontAccessManagerImplTest : public RenderViewHostImplTestHarness {
public:
FontAccessManagerImplTest() {
scoped_feature_list_.InitAndEnableFeature(blink::features::kFontAccess);
}
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
NavigateAndCommit(kTestUrl);
const int process_id = main_rfh()->GetProcess()->GetID();
const int routing_id = main_rfh()->GetRoutingID();
const GlobalFrameRoutingId frame_id =
GlobalFrameRoutingId{process_id, routing_id};
const FontAccessManagerImpl::BindingContext bindingContext = {kTestOrigin,
frame_id};
manager_ = std::make_unique<FontAccessManagerImpl>();
manager_->BindReceiver(bindingContext,
manager_remote_.BindNewPipeAndPassReceiver());
// Set up permission mock.
TestBrowserContext* browser_context =
static_cast<TestBrowserContext*>(main_rfh()->GetBrowserContext());
browser_context->SetPermissionControllerDelegate(
std::make_unique<TestPermissionManager>());
permission_controller_ =
std::make_unique<PermissionControllerImpl>(browser_context);
}
void TearDown() override { RenderViewHostImplTestHarness::TearDown(); }
TestPermissionManager* test_permission_manager() {
return static_cast<TestPermissionManager*>(
main_rfh()->GetBrowserContext()->GetPermissionControllerDelegate());
}
protected:
const GURL kTestUrl = GURL("https://example.com/font_access");
const url::Origin kTestOrigin = url::Origin::Create(GURL(kTestUrl));
std::unique_ptr<FontAccessManagerImpl> manager_;
mojo::Remote<blink::mojom::FontAccessManager> manager_remote_;
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<PermissionControllerImpl> permission_controller_;
};
TEST_F(FontAccessManagerImplTest, NoUserActivationPermissionDenied) {
ASSERT_TRUE(manager_remote_.is_bound() && manager_remote_.is_connected());
base::RunLoop loop;
bool permission_requested = false;
manager_remote_->RequestPermission(
base::BindLambdaForTesting([&](blink::mojom::PermissionStatus status) {
permission_requested = true;
EXPECT_EQ(blink::mojom::PermissionStatus::DENIED, status)
<< "No user activation yields a permission denied status";
loop.Quit();
}));
loop.Run();
EXPECT_TRUE(permission_requested) << "Permission has been requested";
}
TEST_F(FontAccessManagerImplTest, UserActivationPermissionManagerTriggered) {
ASSERT_TRUE(manager_remote_.is_bound() && manager_remote_.is_connected());
// Auto-grant permissions for test.
test_permission_manager()->SetRequestCallback(
base::BindRepeating([](PermissionCallback callback) {
std::move(callback).Run(blink::mojom::PermissionStatus::GRANTED);
}));
// Simulate user activation.
static_cast<RenderFrameHostImpl*>(main_rfh())
->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kInteraction);
base::RunLoop loop;
bool permission_requested = false;
manager_remote_->RequestPermission(
base::BindLambdaForTesting([&](blink::mojom::PermissionStatus status) {
permission_requested = true;
EXPECT_EQ(blink::mojom::PermissionStatus::GRANTED, status)
<< "User activation yields a permission granted status";
loop.Quit();
}));
loop.Run();
EXPECT_TRUE(permission_requested) << "Permission has been requested";
}
} // namespace content
...@@ -1721,6 +1721,7 @@ test("content_unittests") { ...@@ -1721,6 +1721,7 @@ test("content_unittests") {
"../browser/download/save_package_unittest.cc", "../browser/download/save_package_unittest.cc",
"../browser/file_system/browser_file_system_helper_unittest.cc", "../browser/file_system/browser_file_system_helper_unittest.cc",
"../browser/file_system/file_system_operation_runner_unittest.cc", "../browser/file_system/file_system_operation_runner_unittest.cc",
"../browser/font_access/font_access_manager_impl_unittest.cc",
"../browser/frame_host/ancestor_throttle_unittest.cc", "../browser/frame_host/ancestor_throttle_unittest.cc",
"../browser/frame_host/back_forward_cache_metrics_unittest.cc", "../browser/frame_host/back_forward_cache_metrics_unittest.cc",
"../browser/frame_host/clipboard_host_impl_unittest.cc", "../browser/frame_host/clipboard_host_impl_unittest.cc",
......
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
#include "third_party/blink/public/platform/platform.h" #include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h" #include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/modules/font_access/font_iterator.h" #include "third_party/blink/renderer/modules/font_access/font_iterator.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h"
...@@ -22,7 +24,12 @@ void ReturnDataFunction(const v8::FunctionCallbackInfo<v8::Value>& info) { ...@@ -22,7 +24,12 @@ void ReturnDataFunction(const v8::FunctionCallbackInfo<v8::Value>& info) {
} // namespace } // namespace
ScriptValue FontManager::query(ScriptState* script_state) { ScriptValue FontManager::query(ScriptState* script_state,
ExceptionState& exception_state) {
ValidateRequest(ExecutionContext::From(script_state), exception_state);
if (exception_state.HadException())
return ScriptValue();
auto* iterator = auto* iterator =
MakeGarbageCollected<FontIterator>(ExecutionContext::From(script_state)); MakeGarbageCollected<FontIterator>(ExecutionContext::From(script_state));
auto* isolate = script_state->GetIsolate(); auto* isolate = script_state->GetIsolate();
...@@ -44,4 +51,15 @@ void FontManager::Trace(blink::Visitor* visitor) const { ...@@ -44,4 +51,15 @@ void FontManager::Trace(blink::Visitor* visitor) const {
ScriptWrappable::Trace(visitor); ScriptWrappable::Trace(visitor);
} }
void FontManager::ValidateRequest(ExecutionContext* context,
ExceptionState& exception_state) {
DCHECK(context);
auto* frame = To<LocalDOMWindow>(context)->GetFrame();
if (!LocalFrame::HasTransientUserActivation(frame)) {
exception_state.ThrowSecurityError(
"Must be handling a user gesture to enumerate local fonts.");
}
}
} // namespace blink } // namespace blink
...@@ -14,16 +14,20 @@ namespace blink { ...@@ -14,16 +14,20 @@ namespace blink {
class ScriptState; class ScriptState;
class ScriptValue; class ScriptValue;
class ExecutionContext;
class FontManager final : public ScriptWrappable { class FontManager final : public ScriptWrappable {
DEFINE_WRAPPERTYPEINFO(); DEFINE_WRAPPERTYPEINFO();
public: public:
FontManager() = default; FontManager() = default;
ScriptValue query(ScriptState*); ScriptValue query(ScriptState*, ExceptionState&);
DISALLOW_COPY_AND_ASSIGN(FontManager); DISALLOW_COPY_AND_ASSIGN(FontManager);
void Trace(blink::Visitor*) const override; void Trace(blink::Visitor*) const override;
private:
void ValidateRequest(ExecutionContext*, ExceptionState&);
}; };
} // namespace blink } // namespace blink
......
...@@ -9,5 +9,5 @@ ...@@ -9,5 +9,5 @@
SecureContext, SecureContext,
RuntimeEnabled=FontAccess RuntimeEnabled=FontAccess
] interface FontManager { ] interface FontManager {
[CallWith=ScriptState, Measure] object query(); [CallWith=ScriptState, RaisesException, Measure] object query();
}; };
...@@ -341,3 +341,37 @@ async function getUint32(blob, offset) { ...@@ -341,3 +341,37 @@ async function getUint32(blob, offset) {
const dataView = new DataView(buf); const dataView = new DataView(buf);
return dataView.getUint32(0); return dataView.getUint32(0);
} }
function promiseDocumentReady() {
return new Promise(resolve => {
if (document.readyState === 'complete') {
resolve();
}
window.addEventListener('load', () => {
resolve();
}, {once: true});
});
}
async function simulateUserActivation() {
await promiseDocumentReady();
return new Promise(resolve => {
const button = document.createElement('button');
button.textContent = 'Click to enumerate fonts';
button.style.fontSize = '40px';
button.onclick = () => {
document.body.removeChild(button);
resolve();
};
document.body.appendChild(button);
test_driver.click(button);
});
}
function font_access_test(test_function, name, properties) {
return promise_test(async (t) => {
await test_driver.set_permission({name: 'font-access'}, 'granted');
await simulateUserActivation();
await test_function(t, name, properties);
});
}
'use strict'; 'use strict';
promise_test(async t => { font_access_test(async t => {
await test_driver.set_permission({name: 'font-access'}, 'granted');
const iterator = navigator.fonts.query(); const iterator = navigator.fonts.query();
const expectedFonts = await filterEnumeration(iterator, const expectedFonts = await filterEnumeration(iterator,
getEnumerationTestSet({ getEnumerationTestSet({
......
'use strict'; 'use strict';
promise_test(async t => { promise_test(async t => {
await test_driver.set_permission({name: 'font-access'}, 'granted'); assert_throws_dom('SecurityError', () => {
navigator.fonts.query();
});
}, 'query(): fails if there is no user activation');
font_access_test(async t => {
const iterator = navigator.fonts.query(); const iterator = navigator.fonts.query();
assert_equals(typeof iterator, 'object', 'query() should return an Object'); assert_equals(typeof iterator, 'object', 'query() should return an Object');
assert_true(!!iterator[Symbol.asyncIterator], assert_true(!!iterator[Symbol.asyncIterator],
......
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