Introduction
The update_service.dart file in the YourSSH application handles automatic updates—a critical component that downloads and installs new application binaries. However, a flaw in the downloadAsset() function at line 176 created a severe security risk: the service fetched binaries directly from asset.downloadUrl without verifying their cryptographic integrity.
This meant that any attacker who could intercept the network connection—through DNS poisoning, ARP spoofing, or a compromised network—could substitute a legitimate update with malware. The application would dutifully download and install the malicious binary, believing it to be an authentic update.
The Vulnerability Explained
What Was Happening
The vulnerable code in update_service.dart constructed HTTP requests directly from the asset.downloadUrl property without any verification step:
// Vulnerable pattern - downloading without integrity verification
final response = await http.get(Uri.parse(asset.downloadUrl));
// Binary is used directly without checking hash or signature
The ReleaseAsset model only tracked three properties:
class ReleaseAsset {
final String name;
final String downloadUrl;
final int size;
// No digest/hash field existed!
}
The Attack Scenario
Consider this real-world attack against YourSSH:
- Reconnaissance: An attacker identifies that YourSSH checks for updates from GitHub Releases
- Network Position: The attacker gains a man-in-the-middle position (e.g., on a coffee shop WiFi, through DNS poisoning, or via BGP hijacking)
- Interception: When the app calls
downloadAsset(), the attacker intercepts the request - Substitution: Instead of the legitimate binary, the attacker serves a trojanized version with identical filename and similar size
- Execution: The application installs the malicious binary as a "legitimate update"
- Compromise: The attacker now has code execution on the victim's machine
The attack complexity was rated as only "2-step"—position yourself in the network path, then serve the malicious payload. No authentication bypass or complex exploit chain required.
Why HTTPS Wasn't Enough
You might wonder: "Doesn't HTTPS prevent this?" While HTTPS provides transport encryption, it has limitations:
- Compromised update server: If the GitHub account or release pipeline is compromised, HTTPS happily delivers the malicious binary
- Certificate attacks: Rogue certificates, compromised CAs, or SSL stripping can undermine HTTPS
- DNS-level attacks: DNS poisoning can redirect requests before TLS is established
Defense-in-depth requires verifying the binary itself, not just the transport channel.
The Fix
Changes Made
The fix introduced SHA-256 digest verification across multiple files:
1. Added digest field to ReleaseAsset model (app/lib/models/app_release.dart):
class ReleaseAsset {
final String name;
final String downloadUrl;
final int size;
// NEW: "sha256:<hex>" from GitHub API digest field; null when not provided.
final String? digest;
const ReleaseAsset({
required this.name,
required this.downloadUrl,
required this.size,
this.digest, // NEW
});
factory ReleaseAsset.fromJson(Map<String, dynamic> json) => ReleaseAsset(
name: (json['name'] as String?) ?? '',
downloadUrl: (json['browser_download_url'] as String?) ?? '',
size: (json['size'] as num?)?.toInt() ?? 0,
digest: json['digest'] as String?, // NEW: Parse digest from API
);
}
2. Added cryptographic verification to update_service.dart:
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
// In downloadAsset():
// After downloading the binary...
final downloadedBytes = response.bodyBytes;
// Compute SHA-256 of downloaded file
final computedHash = sha256.convert(downloadedBytes);
final computedDigest = 'sha256:${hex.encode(computedHash.bytes)}';
// Verify against expected digest from GitHub API
if (asset.digest != null && computedDigest != asset.digest) {
// Delete the suspicious file
await downloadedFile.delete();
throw UpdateException(
'Binary integrity verification failed. '
'Expected: ${asset.digest}, Got: $computedDigest'
);
}
3. Strengthened URL validation:
The fix also improved the HTTPS scheme check:
// Before: Simple string prefix check (bypassable with malformed URLs)
if (!asset.downloadUrl.startsWith('https://')) { ... }
// After: Proper URI parsing with error handling
final uri = Uri.tryParse(asset.downloadUrl);
if (uri == null || uri.scheme != 'https') {
throw UpdateException('Invalid or non-HTTPS download URL');
}
Why Each Change Matters
| File | Change | Purpose |
|---|---|---|
app_release.dart |
Added digest field |
Captures the trusted hash from GitHub's API |
update_service.dart |
Added crypto imports |
Enables SHA-256 computation |
update_service.dart |
Hash verification logic | Compares computed vs expected digest |
update_service.dart |
File deletion on mismatch | Prevents partial/corrupted installs |
update_service.dart |
Uri.tryParse() validation |
Prevents URL parsing edge cases |
CHANGELOG.md |
Security note | Documents the fix for users |
Prevention & Best Practices
For Update Systems
- Always verify binary integrity: Use cryptographic hashes (SHA-256 minimum) or digital signatures
- Obtain hashes from a trusted source: The hash must come from a channel the attacker can't control (e.g., signed manifest, separate API endpoint)
- Fail closed: If verification fails, abort the update entirely—don't install a potentially malicious binary
- Use code signing: For maximum security, verify GPG/RSA signatures from a known public key
Dart-Specific Recommendations
// Use the crypto package for hash verification
import 'package:crypto/crypto.dart';
import 'package:convert/convert.dart';
Future<bool> verifyBinaryIntegrity(List<int> bytes, String expectedDigest) async {
final hash = sha256.convert(bytes);
final computed = 'sha256:${hex.encode(hash.bytes)}';
return computed == expectedDigest;
}
Detection Tools
- Static analysis: Configure linters to flag HTTP downloads without corresponding hash verification
- Dependency scanning: Ensure
package:cryptois available and used - Integration tests: Add tests that verify the update service rejects tampered binaries
Key Takeaways
- Never trust network downloads implicitly: The
downloadAsset()function assumed the network path was secure—it wasn't - GitHub Releases API provides digests: The
digestfield was available but unused; always check what security metadata your data source provides - URL validation requires proper parsing: String prefix checks (
startsWith('https://')) can be bypassed; useUri.tryParse()for robust validation - Defense-in-depth for updates: HTTPS + hash verification + (ideally) code signing creates multiple barriers for attackers
- Fail safely on verification failure: The fix deletes suspicious files immediately rather than leaving them on disk
How Orbis AppSec Detected This
- Source: The
asset.downloadUrlfield from the GitHub Releases API response, which could be manipulated via MITM attacks - Sink: The file write and installation logic in
update_service.dart:176that processed downloaded bytes without verification - Missing control: No cryptographic hash comparison or signature verification between download and installation
- CWE: CWE-494 (Download of Code Without Integrity Check)
- Fix: Added SHA-256 digest verification using the
cryptopackage, comparing the computed hash against GitHub'sdigestfield before allowing installation
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates why update mechanisms require special security attention. The YourSSH update service had a direct path from network-controlled data to code execution, protected only by HTTPS—which isn't sufficient against determined attackers.
The fix adds a critical verification layer: before any downloaded binary touches the installation process, its SHA-256 hash must match the expected value from GitHub's API. This transforms a "trust the network" model into a "verify then trust" model.
For developers building update systems: always verify binary integrity cryptographically. The few lines of code for hash verification can prevent complete system compromise.