fix: PNGs are not SVGs (and fallback on network image load failure)
What changed, and why it matters
This commit fixes a UI bug where token icons that are PNG images were incorrectly loaded as SVG images, which could fail to display. It also adds a fallback to a local icon file if a network image fails to load. The change is a defensive UI improvement rather than a fix for a known security vulnerability.
No immediate security action required. As a hardening measure, consider validating image URI schemes/hosts and using a sandboxed or restricted image loader to mitigate supply-chain or malicious-icon risks in future releases.
Security signals we found
Network image rendering now distinguishes SVG from non-SVG assets
Error fallback added for failed network image loads
No input sanitization or origin validation added for image URIs
No change to where image URIs originate (still from exchange cache or token metadata)
Evidence from the diff
The patch updates coin_select_item.dart to check the file extension of tokenImageUri before rendering. If the URI ends with .svg, it uses SvgPicture.network; otherwise it uses Image.network with an errorBuilder that falls back to a local SVG file via SvgPicture.file. Previously, all token image URIs were passed to SvgPicture.network, which would fail for PNG/JPEG/etc. The commit also refactors formatting and comments but makes no changes to data validation, network requests, or trust assumptions.
Changed components
lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dartInspect captured patch +66 / −55
diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart
index 4f860c1..9254847 100644
--- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart
+++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart
@@ -48,18 +48,17 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
if (widget.entity is EthTokenEntity) {
ExchangeDataLoadingService.instance.isar.then((isar) async {
- final currency =
- await isar.currencies
- .where()
- .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
- .filter()
- .tokenContractEqualTo(
- (widget.entity as EthTokenEntity).token.address,
- caseSensitive: false,
- )
- .and()
- .imageIsNotEmpty()
- .findFirst();
+ final currency = await isar.currencies
+ .where()
+ .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
+ .filter()
+ .tokenContractEqualTo(
+ (widget.entity as EthTokenEntity).token.address,
+ caseSensitive: false,
+ )
+ .and()
+ .imageIsNotEmpty()
+ .findFirst();
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -75,24 +74,21 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
final solToken = (widget.entity as SolTokenEntity).token;
ExchangeDataLoadingService.instance.isar.then((isar) async {
- final currency =
- await isar.currencies
- .where()
- .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
- .filter()
- .tokenContractEqualTo(
- solToken.address,
- caseSensitive: false,
- )
- .and()
- .imageIsNotEmpty()
- .findFirst();
+ final currency = await isar.currencies
+ .where()
+ .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
+ .filter()
+ .tokenContractEqualTo(solToken.address, caseSensitive: false)
+ .and()
+ .imageIsNotEmpty()
+ .findFirst();
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
- // Use exchange cache image if available, otherwise use logoUri if it's a PNG.
+ // Use exchange cache image if available,
+ // otherwise use logoUri if it's a PNG.
String? fallbackUri;
if (solToken.logoUri != null &&
solToken.logoUri!.endsWith('.png')) {
@@ -116,22 +112,21 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
return Container(
decoration: BoxDecoration(
- color:
- selectedEntity == widget.entity
- ? Theme.of(context).extension<StackColors>()!.textFieldActiveBG
- : Theme.of(context).extension<StackColors>()!.popupBG,
+ color: selectedEntity == widget.entity
+ ? Theme.of(context).extension<StackColors>()!.textFieldActiveBG
+ : Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: MaterialButton(
key: Key(
- "coinSelectItemButtonKey_${widget.entity.name}${widget.entity.ticker}",
+ "coinSelectItemButtonKey_"
+ "${widget.entity.name}${widget.entity.ticker}",
),
- padding:
- isDesktop
- ? const EdgeInsets.only(left: 24)
- : const EdgeInsets.all(12),
+ padding: isDesktop
+ ? const EdgeInsets.only(left: 24)
+ : const EdgeInsets.all(12),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
@@ -143,28 +138,45 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
child: Row(
children: [
tokenImageUri != null
- ? SvgPicture.network(
- tokenImageUri!,
- width: 26,
- height: 26,
- placeholderBuilder: (_) => AppIcon(width: 26, height: 26),
- )
+ ? tokenImageUri!.toLowerCase().endsWith(".svg")
+ ? SvgPicture.network(
+ tokenImageUri!,
+ width: 26,
+ height: 26,
+ placeholderBuilder: (_) =>
+ const AppIcon(width: 26, height: 26),
+ )
+ : Image.network(
+ tokenImageUri!,
+ width: 26,
+ height: 26,
+ errorBuilder: (_, _, _) => SvgPicture.file(
+ File(
+ ref.watch(
+ coinIconProvider(
+ widget.entity.cryptoCurrency,
+ ),
+ ),
+ ),
+ width: 26,
+ height: 26,
+ ),
+ )
: SvgPicture.file(
- File(
- ref.watch(coinIconProvider(widget.entity.cryptoCurrency)),
+ File(
+ ref.watch(
+ coinIconProvider(widget.entity.cryptoCurrency),
+ ),
+ ),
+ width: 26,
+ height: 26,
),
- width: 26,
- height: 26,
- ),
SizedBox(width: isDesktop ? 12 : 10),
Text(
"${widget.entity.name} (${widget.entity.ticker})",
- style:
- isDesktop
- ? STextStyles.desktopTextMedium(context)
- : STextStyles.subtitle600(
- context,
- ).copyWith(fontSize: 14),
+ style: isDesktop
+ ? STextStyles.desktopTextMedium(context)
+ : STextStyles.subtitle600(context).copyWith(fontSize: 14),
),
if (isDesktop && selectedEntity == widget.entity) const Spacer(),
if (isDesktop && selectedEntity == widget.entity)
@@ -175,10 +187,9 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
height: 24,
child: SvgPicture.asset(
Assets.svg.check,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorDark,
),
),
),
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.