Skip to content

Commit 1506b16

Browse files
authored
Merge pull request #2669 from SatoshiPortal/security/audit-fixes-2026-08
fix: resolve 61 security audit findings, mitigate 7, document 5 upstream
2 parents bdb4106 + ff6d9ce commit 1506b16

176 files changed

Lines changed: 4991 additions & 417 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/flutter-setup/action.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,28 @@ runs:
5151
- name: Install FVM
5252
shell: bash
5353
run: |
54-
curl -fsSL https://fvm.app/install.sh | bash -s -- 4.1.2
54+
case "$(uname -m)" in
55+
x86_64) fvm_arch=x64 ;;
56+
aarch64|arm64) fvm_arch=arm64 ;;
57+
*) echo "Unsupported FVM architecture: $(uname -m)" >&2; exit 1 ;;
58+
esac
59+
archive="$RUNNER_TEMP/fvm-4.1.2-linux-${fvm_arch}.tar.gz"
60+
curl --retry 5 --retry-all-errors -fL \
61+
"https://github.com/conceptadev/fvm/releases/download/4.1.2/fvm-4.1.2-linux-${fvm_arch}.tar.gz" \
62+
-o "$archive"
63+
mkdir -p "$HOME/fvm/bin"
64+
# Extract the whole tree, not just `fvm/fvm`: the two release archives
65+
# have different layouts. x64 ships a self-contained binary, arm64 a
66+
# launcher script that execs `src/dart` relative to its own directory —
67+
# unpacking the launcher alone yields `exec: .../src/dart: not found`
68+
# on every fvm invocation.
69+
tar -xzf "$archive" -C "$HOME/fvm/bin" --strip-components=1
70+
chmod +x "$HOME/fvm/bin/fvm"
71+
if [ -f "$HOME/fvm/bin/src/dart" ]; then
72+
chmod +x "$HOME/fvm/bin/src/dart"
73+
fi
74+
# Prove the launcher actually runs before anything depends on it.
75+
"$HOME/fvm/bin/fvm" --version
5576
echo "$HOME/fvm/bin" >> $GITHUB_PATH
5677
5778
- name: Check Flutter version

FEATURES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ graph TB
7474
BUY --> EXCHANGE
7575
BUY --> RECEIVE
7676
BUY --> BULL_PAYJOIN
77+
BUY --> TX_HISTORY
7778
COINS --> UTXO_MGMT
7879
COINS --> LABELS
7980
COINS --> WALLETS
@@ -94,6 +95,7 @@ graph TB
9495
SECRETS --> CORE
9596
SELL --> EXCHANGE
9697
SELL --> BULL_PAYJOIN
98+
SELL --> TX_HISTORY
9799
SEND --> CONSOLIDATION
98100
SEND --> FEES
99101
SEND --> NETWORK

android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,6 @@
7676
<action android:name="android.intent.action.MAIN" />
7777
<category android:name="android.intent.category.LAUNCHER" />
7878
</intent-filter>
79-
<intent-filter>
80-
<action android:name="android.intent.action.VIEW" />
81-
<category android:name="android.intent.category.DEFAULT" />
82-
<category android:name="android.intent.category.BROWSABLE" />
83-
<data android:scheme="bitcoin" />
84-
</intent-filter>
8579
<!-- USB device attached intent filter for BitBox02 -->
8680
<intent-filter>
8781
<action

integration_test/bip85_derivation_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:bb_mobile/core/bip85/data/bip85_datasource.dart';
44
import 'package:bb_mobile/core/bip85/data/bip85_repository.dart';
55
import 'package:bb_mobile/core/bip85/domain/derive_next_bip85_mnemonic_from_default_wallet_usecase.dart';
66
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
7+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
78
import 'package:bb_mobile/core/storage/sqlite_database.dart';
89
import 'package:bb_mobile/core/storage/tables/bip85_derivations_table.dart';
910
import 'package:bb_mobile/core/utils/bip32_derivation.dart';
@@ -50,6 +51,7 @@ Future<void> main({bool isInitialized = false}) async {
5051
bip85Repository: bip85Repository,
5152
walletRepository: walletRepository,
5253
seedRepository: seedRepository,
54+
settingsRepository: locator<SettingsRepository>(),
5355
);
5456

5557
setUpAll(() async {

integration_test/payment_request_test.dart

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,15 @@ Future<void> main({bool isInitialized = false}) async {
4646
expect(bip21.network, Network.bitcoinMainnet);
4747
});
4848

49-
test(
50-
'HTTPS URL with percent-encoded LNAddress in lightning param',
51-
() async {
52-
const input =
53-
'https://admin.bullbitcoin.com/abc'
54-
'?lightning=ishi%40walletofsatoshi.com&label=pleasefundme';
55-
final result = await PaymentRequest.parse(input);
56-
expect(result, isA<LnAddressPaymentRequest>());
57-
expect(
58-
(result as LnAddressPaymentRequest).address,
59-
'ishi@walletofsatoshi.com',
60-
);
61-
},
62-
);
49+
test('HTTPS URL with a percent-encoded LNAddress is rejected', () async {
50+
const input =
51+
'https://admin.bullbitcoin.com/abc'
52+
'?lightning=ishi%40walletofsatoshi.com&label=pleasefundme';
53+
expect(
54+
() => PaymentRequest.parse(input),
55+
throwsA('Invalid payment request'),
56+
);
57+
});
6358
});
6459

6560
group('Liquid address confidentiality (security audit)', () {

ios/Runner/AppDelegate.swift

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import workmanager_apple
99
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1010
) -> Bool {
1111
GeneratedPluginRegistrant.register(with: self)
12+
excludeSensitiveFilesFromBackup()
1213

1314
// workmanager_apple spawns a separate FlutterEngine per background task
1415
// (see BackgroundWorker.swift in workmanager_apple). Plugins registered
@@ -23,23 +24,22 @@ import workmanager_apple
2324
GeneratedPluginRegistrant.register(with: registry)
2425
}
2526

26-
WorkmanagerPlugin.registerPeriodicTask(
27-
withIdentifier: "com.bullbitcoin.mobile.bitcoin-sync-id",
28-
frequency: NSNumber(value: 20 * 60)
29-
)
30-
WorkmanagerPlugin.registerPeriodicTask(
31-
withIdentifier: "com.bullbitcoin.mobile.liquid-sync-id",
32-
frequency: NSNumber(value: 20 * 60)
33-
)
34-
WorkmanagerPlugin.registerPeriodicTask(
35-
withIdentifier: "com.bullbitcoin.mobile.swaps-sync-id",
36-
frequency: NSNumber(value: 20 * 60)
37-
)
38-
WorkmanagerPlugin.registerPeriodicTask(
39-
withIdentifier: "com.bullbitcoin.mobile.logs-prune-id",
40-
frequency: NSNumber(value: 20 * 60)
41-
)
42-
4327
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
4428
}
29+
30+
private func excludeSensitiveFilesFromBackup() {
31+
let fileManager = FileManager.default
32+
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
33+
return
34+
}
35+
36+
// Wallet, payjoin, wallet-engine, and TSV log data all live below Documents.
37+
// Excluding the directory also covers wallet directories created after startup.
38+
var documentsURL = documentsDirectory
39+
var resourceValues = URLResourceValues()
40+
resourceValues.isExcludedFromBackup = true
41+
// `setResourceValues` is `mutating`, so it needs a `var` receiver.
42+
// URLResourceValues.isExcludedFromBackup sets NSURLIsExcludedFromBackupKey.
43+
try? documentsURL.setResourceValues(resourceValues)
44+
}
4545
}

lib/core/bbqr/bbqr.dart

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,25 @@ class Bbqr {
4444
}
4545
} else {
4646
final scannedOptions = BbqrOptions.decode(payload);
47-
if (options != null && scannedOptions.total != options!.total) {
48-
// reset another state.bbqr
49-
// and expect the next scan to be a new BBQR
50-
parts.clear();
51-
return (null, this);
47+
if (options != null &&
48+
(scannedOptions.total != options!.total ||
49+
scannedOptions.encoding != options!.encoding ||
50+
scannedOptions.type != options!.type)) {
51+
_reset();
5252
}
5353

5454
options = scannedOptions;
5555
parts[options!.share] = payload;
5656

5757
if (options!.total == parts.length) {
5858
final bbqrParts = parts.values.toList();
59-
final bbqrJoiner = await bbqr.Joined.tryFromParts(parts: bbqrParts);
59+
late final bbqr.Joined bbqrJoiner;
60+
try {
61+
bbqrJoiner = await bbqr.Joined.tryFromParts(parts: bbqrParts);
62+
} catch (_) {
63+
_reset();
64+
throw FailedToParseBbqr();
65+
}
6066

6167
try {
6268
final tx = await BitcoinTx.fromBytes(bbqrJoiner.data);
@@ -79,13 +85,19 @@ class Bbqr {
7985
);
8086
} catch (_) {}
8187

88+
_reset();
8289
throw FailedToParseBbqr();
8390
} else {
8491
return (null, this);
8592
}
8693
}
8794
}
8895

96+
void _reset() {
97+
parts.clear();
98+
options = null;
99+
}
100+
89101
static Future<List<String>> splitPsbt(String psbt) async {
90102
try {
91103
// check if the PSBT is valid, will throw if not

lib/core/bbqr/bbqr_options.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ class BbqrOptions {
2323
}
2424

2525
factory BbqrOptions.decode(String code) {
26-
if (code.length < 6) throw "Encoded string is too short";
26+
if (code.length < 8) throw "Encoded string is too short";
2727
if (code.substring(0, 2) != "B\$") throw "Invalid header: expected 'B\$'";
2828

2929
final totalQRCodes = int.parse(code.substring(4, 6), radix: 36);
3030
final sequenceNumber = int.parse(code.substring(6, 8), radix: 36);
3131

32+
if (totalQRCodes < 1 || sequenceNumber >= totalQRCodes) {
33+
throw "Invalid BBQR sequence";
34+
}
35+
3236
return BbqrOptions(
3337
encoding: code[2],
3438
type: code[3],

lib/core/bip85/bip85_locator.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:bb_mobile/core/bip85/domain/derive_next_bip85_hex_from_default_w
66
import 'package:bb_mobile/core/bip85/domain/derive_next_bip85_mnemonic_from_default_wallet_usecase.dart';
77
import 'package:bb_mobile/core/bip85/domain/revoke_bip85_derivation_usecase.dart';
88
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
9+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
910
import 'package:bb_mobile/core/storage/sqlite_database.dart';
1011
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
1112
import 'package:get_it/get_it.dart';
@@ -29,6 +30,7 @@ class Bip85DerivationsLocator {
2930
bip85Repository: locator<Bip85Repository>(),
3031
walletRepository: locator<WalletRepository>(),
3132
seedRepository: locator<SeedRepository>(),
33+
settingsRepository: locator<SettingsRepository>(),
3234
),
3335
);
3436

@@ -37,6 +39,7 @@ class Bip85DerivationsLocator {
3739
bip85Repository: locator<Bip85Repository>(),
3840
walletRepository: locator<WalletRepository>(),
3941
seedRepository: locator<SeedRepository>(),
42+
settingsRepository: locator<SettingsRepository>(),
4043
),
4144
);
4245

lib/core/bip85/domain/derive_next_bip85_hex_from_default_wallet_usecase.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import 'package:bb_mobile/core/utils/logger.dart';
77
import 'package:bb_mobile/core/utils/result.dart';
88
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
99
import 'package:meta/meta.dart';
10+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
1011

1112
class DeriveNextBip85HexFromDefaultWalletUsecase {
1213
final Bip85Repository _bip85Repository;
1314
final WalletRepository _walletRepository;
1415
final SeedRepository _seedRepository;
16+
final SettingsRepository _settingsRepository;
1517

1618
DeriveNextBip85HexFromDefaultWalletUsecase({
1719
required this._bip85Repository,
1820
required this._walletRepository,
1921
required this._seedRepository,
22+
required this._settingsRepository,
2023
});
2124

2225
@useResult
@@ -25,9 +28,13 @@ class DeriveNextBip85HexFromDefaultWalletUsecase {
2528
String? alias,
2629
}) async {
2730
try {
31+
// Derive from the default wallet of the environment the app is actually
32+
// running in: a hardcoded mainnet lookup finds no wallet on testnet.
33+
final settings = await _settingsRepository.fetch();
2834
final wallets = await _walletRepository.getWallets(
2935
onlyDefaults: true,
3036
onlyBitcoin: true,
37+
environment: settings.environment,
3138
);
3239
if (wallets.isEmpty) return const Err(Bip85NoDefaultWalletFailure());
3340
final defaultWallet = wallets.first;

0 commit comments

Comments
 (0)