Back to Blog
critical SEVERITY6 min read

How unsigned binary downloads happen in Dart update services and how to fix it

A critical vulnerability in the YourSSH application's update service allowed attackers to serve malicious binaries through man-in-the-middle attacks. The `downloadAsset()` function in `update_service.dart` downloaded application binaries directly from URLs without any cryptographic signature or integrity verification. The fix adds SHA-256 digest validation using the GitHub Releases API's digest field, ensuring only authentic binaries are installed.

O
By Orbis AppSec
Published June 3, 2026Reviewed June 3, 2026

Answer Summary

This vulnerability (CWE-494: Download of Code Without Integrity Check) occurs in Dart when an update service downloads and installs binaries without verifying their cryptographic signature or hash. In `update_service.dart`, the `downloadAsset()` function fetched binaries from `asset.downloadUrl` without validation, enabling MITM attacks. The fix adds SHA-256 digest verification using the `crypto` package, comparing the downloaded file's hash against the GitHub API's `digest` field before installation.

Vulnerability at a Glance

cweCWE-494
fixAdded SHA-256 digest validation using GitHub Releases API digest field
riskRemote code execution via malicious update injection
languageDart (Flutter)
root causeMissing cryptographic verification of downloaded binaries before installation
vulnerabilityDownload of Code Without Integrity Check

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:

  1. Reconnaissance: An attacker identifies that YourSSH checks for updates from GitHub Releases
  2. 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)
  3. Interception: When the app calls downloadAsset(), the attacker intercepts the request
  4. Substitution: Instead of the legitimate binary, the attacker serves a trojanized version with identical filename and similar size
  5. Execution: The application installs the malicious binary as a "legitimate update"
  6. 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

  1. Always verify binary integrity: Use cryptographic hashes (SHA-256 minimum) or digital signatures
  2. 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)
  3. Fail closed: If verification fails, abort the update entirely—don't install a potentially malicious binary
  4. 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:crypto is 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 digest field 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; use Uri.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.downloadUrl field from the GitHub Releases API response, which could be manipulated via MITM attacks
  • Sink: The file write and installation logic in update_service.dart:176 that 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 crypto package, comparing the computed hash against GitHub's digest field 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.

References

Frequently Asked Questions

What is Download of Code Without Integrity Check?

A vulnerability where software downloads and executes code without verifying its authenticity through cryptographic signatures or checksums, allowing attackers to inject malicious code.

How do you prevent unsigned binary downloads in Dart?

Verify downloaded files using cryptographic hashes (SHA-256) or digital signatures before execution, comparing against trusted values from a secure source.

What CWE is Download of Code Without Integrity Check?

CWE-494 (Download of Code Without Integrity Check) covers scenarios where software obtains executable code from a remote location without sufficient integrity verification.

Is HTTPS enough to prevent malicious update injection?

No, HTTPS alone is insufficient. While it prevents passive eavesdropping, compromised update servers, DNS poisoning, or certificate attacks can still serve malicious binaries. Cryptographic verification of the binary itself is essential.

Can static analysis detect unsigned binary downloads?

Yes, static analysis tools can flag code patterns that download files from URLs and execute them without corresponding integrity verification calls like hash comparison or signature validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #32

Related Articles

critical

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in a CGI script that processed HTTP requests using `subprocess.check_output()` with `shell=True`. Attackers could inject arbitrary shell commands through URL parameters using metacharacters like semicolons, pipes, or backticks. The fix converts the command from a string to a list and sets `shell=False`, preventing shell interpretation of user input.

critical

How buffer overflow in URL parsing happens in C++ HTTP client and how to fix it

A critical buffer overflow vulnerability in the HTTP client's URL parsing function allowed attackers to overflow a stack-allocated host buffer through specially crafted URLs with excessively long hostnames. The vulnerability enabled arbitrary code execution by overwriting the return address. The fix adds proper bounds validation before the memcpy() operation to ensure the hostname length never exceeds the destination buffer size.

critical

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

A critical integer overflow vulnerability in `include/compat/dirent_msvc.h` allowed an attacker-controlled directory path length to wrap the `sizeof(wchar_t) * n + 16` allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.

critical

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.

critical

How buffer overflow in memcpy() happens in C/C++ embedded firmware and how to fix it

A critical buffer overflow vulnerability was discovered in the ESP32-based micro-journal firmware where `memcpy()` calls used `strlen()` without bounds checking, allowing oversized USB descriptor strings to corrupt adjacent memory. The fix replaces unbounded `strlen()` with `strnlen()` calls that enforce the destination buffer sizes (8, 16, and 4 bytes respectively), preventing heap/stack corruption from malicious USB devices.

high

How Denial of Service via crafted URI templates happens in Ruby addressable and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-35611) was discovered in the Ruby `addressable` gem versions prior to 2.9.0, which could allow attackers to crash or hang applications by sending specially crafted URI templates. The fix upgrades the dependency from version 2.8.7 to 2.9.0 across the Gemfile, Gemfile.lock, and gemspec in a Fastlane project, eliminating the vulnerable code path entirely.