Skip to content

Commit 36d34d8

Browse files
authored
fix: scam detection issues (#12162)
1 parent a86835c commit 36d34d8

File tree

9 files changed

+160
-71
lines changed

9 files changed

+160
-71
lines changed

packages/mask/content-script/site-adaptors/twitter.com/utils/fetch.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,28 @@ function resolveType(content: string) {
9797
return 'normal'
9898
}
9999

100+
function getVisibleText(node: Node) {
101+
if (node.nodeType === Node.TEXT_NODE) return node.textContent
102+
if (node.nodeType !== Node.ELEMENT_NODE) return ''
103+
if (
104+
(node as Element).getAttribute('aria-hidden') === 'true' && // getBoundingClientRect() causes layout, getAttribute() is cheaper
105+
(node as Element).getBoundingClientRect().width === 0
106+
)
107+
return ''
108+
let text = ''
109+
for (const child of node.childNodes) {
110+
text += getVisibleText(child)
111+
}
112+
return text
113+
}
114+
100115
export function postContentMessageParser(node: HTMLElement): TypedMessage {
101116
function make(node: Node): TypedMessage {
102117
if (node.nodeType === Node.TEXT_NODE) {
103118
if (!node.nodeValue) return makeTypedMessageEmpty()
104119
return makeTypedMessageText(node.nodeValue, getElementStyle(node.parentElement))
105-
} else if (node instanceof HTMLAnchorElement) {
120+
}
121+
if (node instanceof HTMLAnchorElement) {
106122
const anchor = node
107123
const href = anchor.getAttribute('title') ?? anchor.getAttribute('href')
108124
if (href?.includes('/photo/')) {
@@ -131,7 +147,7 @@ export function postContentMessageParser(node: HTMLElement): TypedMessage {
131147
makeTypedMessageEmpty(),
132148
)
133149
}
134-
const content = anchor.textContent
150+
const content = getVisibleText(anchor)
135151
if (!content) return makeTypedMessageEmpty()
136152
const altImage = node.querySelector('img')
137153
return makeTypedMessageAnchor(

packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,23 @@ import { usePopoverControl } from './usePopoverControl.js'
1010
import { WarningCard } from './WarningCard.js'
1111
import { SecurityProvider } from '../../constants.js'
1212
import { GoPlusLabs } from '@masknet/web3-providers'
13+
import { extractAddresses } from '../../utils.js'
14+
import { useDetectAddress } from '../hooks/useDetectAddress.js'
15+
import { AddressTag } from './TextModifier.js'
1316

14-
const useStyles = makeStyles()({
17+
const useStyles = makeStyles()((theme) => ({
1518
link: {
1619
whiteSpace: 'nowrap',
1720
display: 'inline-flex',
1821
alignItems: 'center',
1922
gap: 4,
2023
verticalAlign: 'bottom',
24+
'& > a': {
25+
color: theme.palette.maskColor.danger,
26+
},
27+
},
28+
address: {
29+
display: 'contents',
2130
},
2231
icon: {
2332
width: 18,
@@ -26,20 +35,20 @@ const useStyles = makeStyles()({
2635
overflow: 'hidden',
2736
cursor: 'pointer',
2837
},
29-
})
38+
}))
3039

3140
function isTCO(url: string | null) {
3241
if (!url) return false
3342
return url.startsWith('https://t.co/')
3443
}
3544

36-
export const LinkModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['LinkModifier']>>(function ModifyLink({
45+
export const LinkModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['LinkModifier']>>(function LinkModifier({
3746
fallback,
3847
...props
3948
}) {
4049
const { classes } = useStyles()
4150
const { data } = useQuery({
42-
queryKey: ['scam-warning', 'check-link', props.href],
51+
queryKey: ['scam-warning', 'check-link', props.href, props.children],
4352
queryFn: async () => {
4453
const resolvedLink = isTCO(props.href) ? await resolveTCOLink(props.href) : props.href
4554
if (!resolvedLink) return { isScam: false }
@@ -50,16 +59,35 @@ export const LinkModifier = memo<PropsOf<Plugin.SiteAdaptor.Definition['LinkModi
5059
provider: SecurityProvider.GoPlus,
5160
resolvedLink,
5261
}
62+
const isEllipsis = props.children.endsWith('…')
63+
// We assume that the link contains only one address
64+
const address = isEllipsis ? extractAddresses(resolvedLink, true)[0] : undefined
65+
5366
return {
5467
isScam: await PluginScamRPC.checkUrl(resolvedLink),
5568
provider: SecurityProvider.ScamSniffer,
5669
resolvedLink,
70+
address,
5771
}
5872
},
5973
})
74+
const { data: detected } = useDetectAddress(data?.address, data?.isScam === false)
6075
const { open, anchorEl, iconRef, onMouseEnter, onMouseLeave } = usePopoverControl()
6176

62-
if (!data?.isScam) return fallback
77+
if (!data?.isScam) {
78+
if (detected?.isScam) {
79+
return (
80+
<span className={classes.link}>
81+
<AddressTag className={classes.address} address={data!.address!} nested text="" />
82+
<Link href={props.href} target="_blank" rel="noopener noreferrer" fontSize="inherit">
83+
{props.children}
84+
{props.suggestedPostImage}
85+
</Link>
86+
</span>
87+
)
88+
}
89+
return fallback
90+
}
6391

6492
return (
6593
<span className={classes.link}>

packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
import { Icons } from '@masknet/icons'
22
import type { Plugin } from '@masknet/plugin-infra'
33
import { makeStyles, ShadowRootPopper } from '@masknet/theme'
4-
import { GoPlusLabs } from '@masknet/web3-providers'
5-
import { isValidAddress } from '@masknet/web3-shared-evm'
6-
import { isValidAddress as isSolAddress } from '@masknet/web3-shared-solana'
7-
import { useQuery } from '@tanstack/react-query'
84
import { Fragment, memo, useMemo } from 'react'
9-
import { EVM_ADDRESS, SecurityProvider, SOLANA_ADDRESS, TRON_ADDRESS } from '../../constants.js'
10-
import { PluginScamRPC } from '../../messages.js'
11-
import { isTronAddress } from '../../utils.js'
5+
import { extractAddresses } from '../../utils.js'
126
import { usePopoverControl } from './usePopoverControl.js'
137
import { WarningCard } from './WarningCard.js'
8+
import { useDetectAddress } from '../hooks/useDetectAddress.js'
149

15-
const useStyles = makeStyles()({
10+
const useStyles = makeStyles()((theme) => ({
1611
text: {
1712
whiteSpace: 'nowrap',
1813
display: 'inline-flex',
1914
alignItems: 'center',
2015
gap: 4,
2116
verticalAlign: 'bottom',
17+
color: theme.palette.maskColor.danger,
2218
},
2319
icon: {
2420
width: 18,
@@ -27,48 +23,33 @@ const useStyles = makeStyles()({
2723
overflow: 'hidden',
2824
cursor: 'pointer',
2925
},
30-
})
26+
}))
3127

3228
type TextModifierProps = PropsOf<Plugin.SiteAdaptor.Definition['TextModifier']>
3329

3430
interface AddressTagProps {
3531
address: string
3632
text: string
33+
nested?: boolean
34+
className?: string
3735
}
3836

39-
const AddressTag = memo<AddressTagProps>(function AddressTag({ address, text }) {
40-
const { classes } = useStyles()
37+
export const AddressTag = memo<AddressTagProps>(function AddressTag({ address, text, className, nested }) {
38+
const { classes, cx } = useStyles()
4139
const { open, anchorEl, iconRef, onMouseEnter, onMouseLeave } = usePopoverControl()
42-
const { data } = useQuery({
43-
queryKey: ['detect-address', address],
44-
queryFn: async () => {
45-
if (isValidAddress(address)) {
46-
return { isScam: await PluginScamRPC.checkAddress(address), provider: SecurityProvider.ScamSniffer }
47-
}
48-
if (isSolAddress(address))
49-
return {
50-
isScam: await GoPlusLabs.checkIfAddressIsScam('solana', address),
51-
provider: SecurityProvider.GoPlus,
52-
}
53-
if (isTronAddress(address))
54-
return {
55-
isScam: GoPlusLabs.checkIfAddressIsScam('tron', address),
56-
provider: SecurityProvider.GoPlus,
57-
}
58-
return { isScam: false, provider: null }
59-
},
60-
})
40+
const { data } = useDetectAddress(address)
41+
6142
if (!data?.isScam) return text
6243
return (
63-
<span className={classes.text}>
44+
<span className={cx(classes.text, className)}>
6445
<Icons.Danger
6546
size={18}
6647
className={classes.icon}
6748
ref={iconRef}
6849
onMouseEnter={onMouseEnter}
6950
onMouseLeave={onMouseLeave}
7051
/>
71-
{address}
52+
{nested ? null : address}
7253
<ShadowRootPopper open={open} anchorEl={anchorEl}>
7354
<WarningCard
7455
address={address}
@@ -88,12 +69,7 @@ interface Segment {
8869
}
8970

9071
export const TextModifier = memo<TextModifierProps>(function TextModifier({ fallback, children: fullText }) {
91-
const addresses = useMemo(() => {
92-
const evmAddresses = fullText.match(EVM_ADDRESS) || []
93-
const solAddresses = fullText.match(SOLANA_ADDRESS) || []
94-
const tronAddresses = fullText.match(TRON_ADDRESS) || []
95-
return [...evmAddresses, ...solAddresses, ...tronAddresses]
96-
}, [fullText])
72+
const addresses = useMemo(() => extractAddresses(fullText), [fullText])
9773

9874
const segments = useMemo(() => {
9975
let leftOffset = 0

packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { t } from '@lingui/core/macro'
22
import { Trans } from '@lingui/react/macro'
33
import { Icons } from '@masknet/icons'
4-
import { LoadingBase, makeStyles } from '@masknet/theme'
5-
import { Button, Typography } from '@mui/material'
6-
import { memo, useState, type HTMLProps } from 'react'
4+
import { makeStyles } from '@masknet/theme'
5+
import { Typography } from '@mui/material'
6+
import { memo, type HTMLProps } from 'react'
77
import { SecurityProvider } from '../../constants.js'
88

99
const useStyles = makeStyles()((theme) => ({
@@ -65,22 +65,11 @@ const useStyles = makeStyles()((theme) => ({
6565
minWidth: 0,
6666
textOverflow: 'ellipsis',
6767
overflow: 'hidden',
68+
whiteSpace: 'nowrap',
6869
},
6970
link: {
7071
textDecoration: 'underline',
7172
},
72-
reportButton: {
73-
padding: theme.spacing(1, 0),
74-
width: 60,
75-
minWidth: 60,
76-
textAlign: 'center',
77-
alignItems: 'center',
78-
justifyContent: 'center',
79-
borderRadius: 32,
80-
color: theme.palette.maskColor.main,
81-
backgroundColor: theme.palette.maskColor.thirdMain,
82-
marginLeft: 'auto',
83-
},
8473
description: {
8574
fontFamily: 'Helvetica',
8675
borderRadius: 8,
@@ -101,7 +90,6 @@ interface Props extends HTMLProps<HTMLDivElement> {
10190

10291
export const WarningCard = memo(function WarningCard({ link, address, securityProvider, ...rest }: Props) {
10392
const { classes, cx } = useStyles()
104-
const [isReporting] = useState(false)
10593
return (
10694
<div {...rest} className={cx(classes.card, rest.className)}>
10795
<div className={classes.header}>
@@ -130,15 +118,13 @@ export const WarningCard = memo(function WarningCard({ link, address, securityPr
130118
</div>
131119
<div className={classes.content}>
132120
{link ?
133-
<a className={cx(classes.link, classes.target)} href={link}>
121+
<a className={cx(classes.link, classes.target)} href={link} title={link}>
134122
{link}
135123
</a>
136-
: <Typography className={classes.target}>{address}</Typography>}
137-
<Button variant="text" className={classes.reportButton}>
138-
{isReporting ?
139-
<LoadingBase size={16} />
140-
: <Icons.Flag size={16} />}
141-
</Button>
124+
: <Typography className={classes.target} title={address}>
125+
{address}
126+
</Typography>
127+
}
142128
</div>
143129
<Typography className={classes.description}>
144130
<Trans>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { resolveTCOLink } from '@masknet/plugin-infra/dom/context'
2+
import { GoPlusLabs } from '@masknet/web3-providers'
3+
import { useQuery } from '@tanstack/react-query'
4+
import { SecurityProvider } from '../../constants.js'
5+
import { PluginScamRPC } from '../../messages.js'
6+
import { extractAddresses } from '../../utils.js'
7+
8+
function isTCO(url: string | null) {
9+
if (!url) return false
10+
return url.startsWith('https://t.co/')
11+
}
12+
13+
export function useCheckLink(link: string, text: string) {
14+
return useQuery({
15+
queryKey: ['scam-warning', 'check-link', link, text],
16+
queryFn: async () => {
17+
const resolvedLink = isTCO(link) ? await resolveTCOLink(link) : link
18+
if (!resolvedLink) return { isScam: false }
19+
const result = await GoPlusLabs.checkIsPhishingSite(resolvedLink)
20+
if (result)
21+
return {
22+
isScam: result,
23+
provider: SecurityProvider.GoPlus,
24+
resolvedLink,
25+
}
26+
const isEllipsis = text.endsWith('…')
27+
// We assume that the link contains only one address
28+
const address = isEllipsis ? extractAddresses(resolvedLink)[0] : undefined
29+
30+
return {
31+
isScam: await PluginScamRPC.checkUrl(resolvedLink),
32+
provider: SecurityProvider.ScamSniffer,
33+
resolvedLink,
34+
address,
35+
}
36+
},
37+
})
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { GoPlusLabs } from '@masknet/web3-providers'
2+
import { isValidAddress } from '@masknet/web3-shared-evm'
3+
import { isValidAddress as isSolAddress } from '@masknet/web3-shared-solana'
4+
import { useQuery } from '@tanstack/react-query'
5+
import { SecurityProvider } from '../../constants.js'
6+
import { PluginScamRPC } from '../../messages.js'
7+
import { isTronAddress } from '../../utils.js'
8+
9+
export function useDetectAddress(address: string | null | undefined, enabled = true) {
10+
return useQuery({
11+
enabled: !!address && enabled,
12+
queryKey: ['detect-address', address],
13+
queryFn: async () => {
14+
if (!address) return null
15+
if (isValidAddress(address)) {
16+
return { isScam: await PluginScamRPC.checkAddress(address), provider: SecurityProvider.ScamSniffer }
17+
}
18+
if (isSolAddress(address))
19+
return {
20+
isScam: await GoPlusLabs.checkIfAddressIsScam('solana', address),
21+
provider: SecurityProvider.GoPlus,
22+
}
23+
if (isTronAddress(address))
24+
return {
25+
isScam: GoPlusLabs.checkIfAddressIsScam('tron', address),
26+
provider: SecurityProvider.GoPlus,
27+
}
28+
return { isScam: false, provider: null }
29+
},
30+
})
31+
}

packages/plugins/ScamWarning/src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export const EVM_ADDRESS = /(^|\s)(0x[a-fA-F0-9]{40})/gu
88
export const SOLANA_ADDRESS = /(^|\s)([1-9A-HJ-NP-Za-km-z]{32,44})/gu
99
export const TRON_ADDRESS = /(^|\s)(T[A-Za-z1-9]{33})/gu
1010

11+
export const EXIST_EVM_ADDRESS = /\b(0x[a-fA-F0-9]{40})/gu
12+
export const EXIST_SOLANA_ADDRESS = /\b([1-9A-HJ-NP-Za-km-z]{32,44})/gu
13+
export const EXIST_TRON_ADDRESS = /\b(T[A-Za-z1-9]{33})/gu
14+
1115
export enum SecurityProvider {
1216
ScamSniffer = 'ScamSniffer',
1317
GoPlus = 'GoPlus',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import { EVM_ADDRESS, SOLANA_ADDRESS } from '@masknet/plugin-scam-warning'
2+
import { EXIST_EVM_ADDRESS, EXIST_SOLANA_ADDRESS, EXIST_TRON_ADDRESS, TRON_ADDRESS } from './constants.js'
3+
14
export function isTronAddress(address: string) {
25
return !!address.match(address)
36
}
7+
8+
export function extractAddresses(text: string, exist = false) {
9+
const evmAddresses = text.match(exist ? EXIST_EVM_ADDRESS : EVM_ADDRESS) || []
10+
const solAddresses = text.match(exist ? EXIST_SOLANA_ADDRESS : SOLANA_ADDRESS) || []
11+
const tronAddresses = text.match(exist ? EXIST_TRON_ADDRESS : TRON_ADDRESS) || []
12+
return [...evmAddresses, ...solAddresses, ...tronAddresses]
13+
}

0 commit comments

Comments
 (0)