Chrome Extensions URL Blocking Integration Guide
This guide delineates the steps to seamlessly incorporate the Phishing Detection URL Scanning API into your Web3 wallet. By leveraging this API, you can fortify your wallet's security in three pivotal interfaces:
- Transaction Approval Screen: Before a user approves a transaction, validate the legitimacy of the associated domain.
- Message Signing Screen: Ensure the domain requesting a message signature is trustworthy and devoid of threats.
- Blocking wallet drainers: Proactively scan & block malicious URLs before your users interact or even connect their wallet. (this guide covers this case)
For this guide, let's focus on use case #3. We will integrate the screen below which is the page that our Chrome Extension shows when blocking a malicious website suspected of being a wallet drainer.
Table of contents
- Getting familiar with the API
- Reference material
- Chrome extension integration guide
- Additional improvements
- Best practices
Getting familiar with the API
API Details
Endpoint
The scan endpoint of the Wallet Guard API checks domain URLs for phishing threats. By integrating it into your Web3 wallet, you can quickly alert users to suspicious domains, enhancing security.
Base URL: https://api.walletguard.app/v1/scan
Request Parameters
url
(required): The domain URL you wish to evaluate.
Headers
X-API-KEY
: Your unique API key, provided by us.
Scan Result Example
GET <https://api.walletguard.app/v1/scan?url=stake-pepe.com>
Here's a sample response you might receive when scanning a malicious domain:
{
"domainName": "stake-pepe.com",
"recommendedAction": "BLOCK",
"riskFactors": [
{
"type": "DRAINER",
"severity": "CRITICAL",
"message": "Domain identified as a wallet drainer."
},
{
"type": "RECENTLY_CREATED",
"severity": "HIGH",
"message": "Domain recently created.",
"value": "72" // indicates the domain was created 72 hours ago
},
{
"type": "ML_INFERENCE",
"severity": "HIGH",
"message": "Domain is likely a phishing attempt.",
"value": "pepe.vip" // the OFFICIAL site that is being impersonated
}
],
"verified": false,
"status": "COMPLETE"
}
Models
We recommend copy and pasting in the models before starting.
Reference Material
Our chrome extension is open source, so all the code here can be referenced. The extension still uses the v0 endpoints for detecting malicious URLs, but functionally there is no difference between v0 and v1. The only difference is the improved model which is easier to use.
https://github.com/wallet-guard/wallet-guard-extension
Chrome extension integration guide
The below code hooks into URL updates using the chrome.tabs
API to show a block page if the recommended action is BLOCK
and a chrome notification for the WARN
recommended action.
// PHISHING DETECTION
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// some sites like exocharts trigger onUpdated events, but the url hasn't changed causing excessive function calls
if (changeInfo?.url === undefined) return;
try {
const scanResult = await checkPhishing(changeInfo.url, my_api_key);
// If the recommended action is to block, redirect to a blocking page.
if (scanResult.recommendedAction === RecommendedAction.Block) {
chrome.tabs.update(tab.id, { url: "blocked_page.html" });
// If the recommended action is to warn, show a notification.
// We DO NOT recommend using chrome notifications in production as it may introduce false positives.
// A better place to show this data is inside transaction simulation, where the warning may be more relvant.
} else if (scanResult.recommendedAction === RecommendedAction.Warn) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'Warning',
message: 'This site may contain phishing indicators!'
});
}
} catch (e) {
console.error('unknown error fetching scan result', e);
}
});
// Function to send a request to the phishing detection API.
async function checkPhishing(url: string, apiKey: string): Promise<ScanResult> {
// Constructing the endpoint URL with the given domain.
const endpoint = `https://api.walletguard.app/v1/scan?url=${url}`;
// Sending a GET request to the API.
const response = await fetch(endpoint, {
headers: {
'x-api-key': apiKey
}
});
// If the response is not successful, throw an error.
if (!response.ok) {
throw new Error(`API call failed with status ${response.status}`);
}
// Parse and return the JSON response from the API.
return response.json();
}
🎉 It's that easy. The above code is good enough for a basic proof of concept. We recommend going over the additional improvements section for optimizations.
Quick notes here for the above code:
- It is not recommended to store your API key inside your Chrome extension. We recommend integrating the scan endpoint on your own server for best practice on security. We can work with you on solutions however if this is not feasible.
- WARNING: Usage of chrome.tabs requires the
tabs
permission in your manifest file. This permission will trigger a warning and disable the extension for existing users if you do not have it. There are other methods of detecting URL changes. Read the following Chrome guide for more information.
Additional improvements
Check that the domain has changed before re-requesting
We can add the following code to background.ts
so that we do not request again until the domain has changed. For example, going from opensea.io
to opensea.io/some_collection
triggers a URL change event within the chrome.tabs API.
import { parseDomain, ParseResultType, ParseDomainListed } from 'parse-domain';
//... background.ts ...
const latestScan = await chrome.storage.local.get('latestScan')?.latestScan;
const domainChanged = domainHasChanged(changeInfo.url, latestScan);
if (!domainChanged) return;
const scanResult = await checkPhishing(changeInfo.url, my_api_key);
// Update the current scan in localstorage
chrome.storage.local.set({ latestScan: scanResult });
// If the recommended action is to block, redirect to a blocking page.
if (scanResult.recommendedAction === RecommendedAction.Block) {
// ...
}
// ... background.ts ...
function domainHasChanged(
url: string,
currentSite: PhishingResponse
): boolean {
const domainObj = parseDomain(standardizeUrl(url));
if (domainObj.type !== ParseResultType.Listed) return true;
const domainName = createDomainName(domainObj);
return domainName !== currentSite.domainName;
}
function standardizeUrl(url: string): string {
url = url.replace('https://', '');
url = url.replace('http://', '');
url = url.replace('www.', '');
const backslashIndex = url.indexOf('/');
if (backslashIndex !== -1) {
url = url.substring(0, backslashIndex);
}
return url;
}
function createDomainName(domainObj: ParseResultListed) {
const domain = domainObj.domain;
const topLevelDomains = domainObj.topLevelDomains;
// Handle URLs like gov.uk, netlify.app, etc.
if (!domain) {
return topLevelDomains.join('.');
}
let domainExtension = '';
topLevelDomains.forEach((topLevelDomain) => {
domainExtension += '.' + topLevelDomain;
});
return domainObj.domain + domainExtension;
}
Prevent blocking the same site continuously by using a local allowlist
// .. background.ts ..
const personalAllowlist: string[] = await chrome.storage.local.get('personalAllowlist')?.personalAllowlist;
const scanResult = await checkPhishing(changeInfo.url, my_api_key);
// If the user has clicked the Proceed anyway button, no longer block the URL.
if (personalAllowlist.includes(scanResult.domainName)) return;
Best Practices
Use domainName as your key
DomainName
is the domain + top-level domain combination that we use as keys for website scans. This strips away all https://
, www.
and everything in the path of a URL.
Handling IN_PROGRESS
Status
IN_PROGRESS
StatusIf a response returns the status IN_PROGRESS
, it indicates that the domain is new or the previous scan expired. The website is then being assessed for advanced heuristics. Implement a polling mechanism as described below:
- Wait for 30 seconds after the initial request.
- Send another request to the same endpoint.
- If the returned status remains
IN_PROGRESS
, pause for an additional 5 seconds and re-request. - Repeat the retry logic 2 more times.
Typically, the scan completes after the first retry. Persistent IN_PROGRESS
status might indicate an internal issue.
Updated 11 months ago