Skip to content

Commit

Permalink
Verify NFT content media type before pinning (#17392)
Browse files Browse the repository at this point in the history
* Verify NFT content media type before pinning
Allow pinnning only if metadata and image has ipfs format
Resolves brave/brave-browser#28775
  • Loading branch information
cypt4 committed Mar 6, 2023
1 parent f0b1d5f commit bf8d546
Show file tree
Hide file tree
Showing 5 changed files with 476 additions and 26 deletions.
7 changes: 6 additions & 1 deletion browser/brave_wallet/brave_wallet_pin_service_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "chrome/browser/profiles/incognito_helpers.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/storage_partition.h"

namespace brave_wallet {

Expand Down Expand Up @@ -86,7 +87,11 @@ KeyedService* BraveWalletPinServiceFactory::BuildServiceInstanceFor(
user_prefs::UserPrefs::Get(context),
JsonRpcServiceFactory::GetServiceForContext(context),
ipfs::IpfsLocalPinServiceFactory::GetServiceForContext(context),
ipfs::IpfsServiceFactory::GetForContext(context));
ipfs::IpfsServiceFactory::GetForContext(context),
std::make_unique<ContentTypeChecker>(
user_prefs::UserPrefs::Get(context),
context->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess()));
}

content::BrowserContext* BraveWalletPinServiceFactory::GetBrowserContextToUse(
Expand Down
165 changes: 151 additions & 14 deletions components/brave_wallet/browser/brave_wallet_pin_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "brave/components/ipfs/ipfs_constants.h"
#include "brave/components/ipfs/ipfs_utils.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "services/network/public/cpp/resource_request.h"

namespace brave_wallet {

Expand Down Expand Up @@ -78,13 +79,19 @@ absl::optional<mojom::WalletPinServiceErrorCode> StringToErrorCode(
return mojom::WalletPinServiceErrorCode::ERR_NOT_PINNED;
} else if (error == "ERR_PINNING_FAILED") {
return mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED;
} else if (error == "ERR_MEDIA_TYPE_UNSUPPORTED") {
return mojom::WalletPinServiceErrorCode::ERR_MEDIA_TYPE_UNSUPPORTED;
}
return absl::nullopt;
}

absl::optional<std::string> ExtractCID(const std::string& ipfs_url) {
GURL gurl = GURL(ipfs_url);

if (!gurl.is_valid()) {
return absl::nullopt;
}

if (!gurl.SchemeIs(ipfs::kIPFSScheme)) {
return absl::nullopt;
}
Expand Down Expand Up @@ -149,11 +156,97 @@ std::string BraveWalletPinService::ErrorCodeToString(
return "ERR_NOT_PINNED";
case mojom::WalletPinServiceErrorCode::ERR_PINNING_FAILED:
return "ERR_PINNING_FAILED";
case mojom::WalletPinServiceErrorCode::ERR_MEDIA_TYPE_UNSUPPORTED:
return "ERR_MEDIA_TYPE_UNSUPPORTED";
}
NOTREACHED();
return "";
}

ContentTypeChecker::ContentTypeChecker(
PrefService* pref_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: pref_service_(pref_service),
url_loader_factory_(std::move(url_loader_factory)) {}

ContentTypeChecker::ContentTypeChecker() = default;

ContentTypeChecker::~ContentTypeChecker() = default;

void ContentTypeChecker::CheckContentTypeSupported(
const std::string& ipfs_url,
base::OnceCallback<void(absl::optional<bool>)> callback) {
// Create a request with no data or cookies.
auto resource_request = std::make_unique<network::ResourceRequest>();
GURL translated_url;
if (!ipfs::TranslateIPFSURI(GURL(ipfs_url), &translated_url,
ipfs::GetDefaultNFTIPFSGateway(pref_service_),
false)) {
std::move(callback).Run(false);
return;
}
resource_request->url = translated_url;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->redirect_mode = ::network::mojom::RedirectMode::kFollow;

auto annotation =
net::DefineNetworkTrafficAnnotation("brave_wallet_pin_service", R"(
semantics {
sender: "Brave wallet pin service"
description:
"This service is used to pin NFTs"
"which are added to the wallet."
trigger:
"Triggered by enable auto-pinning mode"
"from the Brave Wallet page or settings."
data:
"Options of the commands."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"You can enable or disable this feature in brave://settings."
policy_exception_justification:
"Not implemented."
}
)");

auto url_loader =
network::SimpleURLLoader::Create(std::move(resource_request), annotation);
auto* url_loader_ptr = url_loader.get();
auto it = loaders_in_progress_.insert(loaders_in_progress_.end(),
std::move(url_loader));

url_loader_ptr->SetTimeoutDuration(base::Seconds(60));
url_loader_ptr->SetRetryOptions(
5, network::SimpleURLLoader::RetryMode::RETRY_ON_5XX |
network::SimpleURLLoader::RetryMode::RETRY_ON_NETWORK_CHANGE);
url_loader_ptr->DownloadHeadersOnly(
url_loader_factory_.get(),
base::BindOnce(&ContentTypeChecker::OnHeadersFetched,
weak_ptr_factory_.GetWeakPtr(), std::move(it),
std::move(callback)));
}

void ContentTypeChecker::OnHeadersFetched(
UrlLoaderList::iterator loader_it,
base::OnceCallback<void(absl::optional<bool>)> callback,
scoped_refptr<net::HttpResponseHeaders> headers) {
loaders_in_progress_.erase(loader_it);
if (!headers) {
std::move(callback).Run(absl::nullopt);
return;
}
std::string content_type_value;
headers->GetMimeType(&content_type_value);
if (base::StartsWith(content_type_value, "image/")) {
std::move(callback).Run(true);
} else {
std::move(callback).Run(false);
}
}

// static
bool BraveWalletPinService::IsTokenSupportedForPinning(
const mojom::BlockchainTokenPtr& token) {
Expand Down Expand Up @@ -204,11 +297,13 @@ BraveWalletPinService::BraveWalletPinService(
PrefService* prefs,
JsonRpcService* service,
ipfs::IpfsLocalPinService* local_pin_service,
IpfsService* ipfs_service)
IpfsService* ipfs_service,
std::unique_ptr<ContentTypeChecker> content_type_checker)
: prefs_(prefs),
json_rpc_service_(service),
local_pin_service_(local_pin_service),
ipfs_service_(ipfs_service) {
ipfs_service_(ipfs_service),
content_type_checker_(std::move(content_type_checker)) {
ipfs_service_->AddObserver(this);
}

Expand Down Expand Up @@ -480,8 +575,8 @@ void BraveWalletPinService::OnTokenMetaDataReceived(
return;
}

GURL token_gurl = GURL(token_url);
if (!token_gurl.SchemeIs(ipfs::kIPFSScheme)) {
auto metadata_cid = ExtractCID(token_url);
if (!metadata_cid) {
auto pin_error = mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL,
"Metadata has non-ipfs url");
Expand All @@ -504,23 +599,65 @@ void BraveWalletPinService::OnTokenMetaDataReceived(
return;
}

auto* image_url = parsed_result->FindStringKey("image");
auto image_cid =
image_url != nullptr ? ExtractCID(*image_url) : absl::nullopt;

if (!image_cid) {
auto pin_error = mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL,
"Can't find proper image field");
SetTokenStatus(service, token,
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, pin_error);
std::move(callback).Run(false, std::move(pin_error));
return;
}

std::vector<std::string> cids;
cids.push_back(metadata_cid.value());
cids.push_back(image_cid.value());

cids.push_back(ExtractCID(token_url).value());
auto* image = parsed_result->FindStringKey("image");
if (image) {
auto image_cid = ExtractCID(*image);
if (image_cid) {
cids.push_back(image_cid.value());
}
content_type_checker_->CheckContentTypeSupported(
*image_url,
base::BindOnce(&BraveWalletPinService::OnContentTypeChecked,
weak_ptr_factory_.GetWeakPtr(), std::move(service),
std::move(token), std::move(cids), std::move(callback)));
}

void BraveWalletPinService::OnContentTypeChecked(
absl::optional<std::string> service,
mojom::BlockchainTokenPtr token,
std::vector<std::string> cids,
AddPinCallback callback,
absl::optional<bool> result) {
if (!result.has_value()) {
SetTokenStatus(service, token,
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, nullptr);
std::move(callback).Run(
false, mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_FETCH_METADATA_FAILED,
"Failed to verify media type"));
return;
}

if (!result.value()) {
SetTokenStatus(service, token,
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, nullptr);
std::move(callback).Run(
false, mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_MEDIA_TYPE_UNSUPPORTED,
"Media type not supported"));
return;
}

auto path = GetTokenPrefPath(service, token);
if (!path) {
SetTokenStatus(service, token,
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, nullptr);
std::move(callback).Run(
false,
mojom::PinError::New(mojom::WalletPinServiceErrorCode::ERR_WRONG_TOKEN,
"Wrong token data"));
false, mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_WRONG_METADATA_FORMAT,
"Wrong token data"));
return;
}

Expand Down
50 changes: 46 additions & 4 deletions components/brave_wallet/browser/brave_wallet_pin_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_
#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BRAVE_WALLET_PIN_SERVICE_H_

#include <list>
#include <memory>
#include <set>
#include <string>
#include <vector>
Expand All @@ -23,10 +25,42 @@
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace brave_wallet {

class ContentTypeChecker {
public:
ContentTypeChecker(
PrefService* pref_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
virtual ~ContentTypeChecker();

virtual void CheckContentTypeSupported(
const std::string& cid,
base::OnceCallback<void(absl::optional<bool>)> callback);

protected:
// For tests
ContentTypeChecker();

private:
using UrlLoaderList = std::list<std::unique_ptr<network::SimpleURLLoader>>;

void OnHeadersFetched(UrlLoaderList::iterator iterator,
base::OnceCallback<void(absl::optional<bool>)> callback,
scoped_refptr<net::HttpResponseHeaders> headers);

raw_ptr<PrefService> pref_service_;
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
// List of requests are actively being sent.
UrlLoaderList loaders_in_progress_;

base::WeakPtrFactory<ContentTypeChecker> weak_ptr_factory_{this};
};

/**
* At the moment only local pinning is supported so use absl::nullopt
* for optional service argument.
Expand All @@ -35,10 +69,12 @@ class BraveWalletPinService : public KeyedService,
public brave_wallet::mojom::WalletPinService,
public ipfs::IpfsServiceObserver {
public:
BraveWalletPinService(PrefService* prefs,
JsonRpcService* service,
ipfs::IpfsLocalPinService* local_pin_service,
IpfsService* ipfs_service);
BraveWalletPinService(
PrefService* prefs,
JsonRpcService* service,
ipfs::IpfsLocalPinService* local_pin_service,
IpfsService* ipfs_service,
std::unique_ptr<ContentTypeChecker> content_type_checker);
~BraveWalletPinService() override;

virtual void Restore();
Expand Down Expand Up @@ -134,6 +170,11 @@ class BraveWalletPinService : public KeyedService,
const std::string& result,
mojom::ProviderError error,
const std::string& error_message);
void OnContentTypeChecked(absl::optional<std::string> service,
mojom::BlockchainTokenPtr token,
std::vector<std::string> cids,
AddPinCallback callback,
absl::optional<bool> result);

// ipfs::IpfsServiceObserver
void OnIpfsLaunched(bool result, int64_t pid) override;
Expand All @@ -149,6 +190,7 @@ class BraveWalletPinService : public KeyedService,
raw_ptr<JsonRpcService> json_rpc_service_ = nullptr;
raw_ptr<ipfs::IpfsLocalPinService> local_pin_service_ = nullptr;
raw_ptr<IpfsService> ipfs_service_ = nullptr;
std::unique_ptr<ContentTypeChecker> content_type_checker_;

base::WeakPtrFactory<BraveWalletPinService> weak_ptr_factory_{this};
};
Expand Down
Loading

0 comments on commit bf8d546

Please sign in to comment.