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 heap buffer overflow happens in C UART response handling and how to fix it

A critical heap buffer overflow vulnerability was discovered in the AT client response handler (`sm_at_client.c`) where incoming UART data was copied into a fixed-size buffer without verifying available capacity. A compromised modem or malicious UART data could trigger arbitrary heap corruption. The fix replaces an assertion-only guard with proper bounds clamping using `MIN()` to ensure writes never exceed the `at_cmd_resp` buffer allocation.

critical

How out-of-bounds read via unchecked memcpy happens in C packet processing and how to fix it

A critical out-of-bounds read vulnerability was discovered in `hep-tester/heptester.c` where `memcpy` calls at lines 200-201 read from fixed offsets in a packet buffer without verifying the buffer was large enough. An attacker could send a crafted packet shorter than 18 bytes to trigger the read, potentially leaking memory contents or crashing the capture agent. The fix adds a single bounds check against `pkthdr->caplen` before any memory copy operations.

high

NULL Pointer Dereference in ESP8266 user_interface.c wifi_station_set_default_hostname()

A critical NULL pointer dereference vulnerability in the ESP8266 firmware's `user_interface.c` allowed attackers to crash devices by exhausting the limited 80KB heap memory. The `wifi_station_set_default_hostname()` function's `os_malloc` call lacked a proper NULL guard, causing `ets_sprintf` to write to address 0 when allocation failed. The fix corrected a logic inversion in the NULL check condition.

high

Integer Overflow in PlayerAnimation.cpp memcpy Size Calculations

A critical integer overflow vulnerability was discovered in `animation/PlayerAnimation.cpp` where `vCount * sizeof(float) * 3` calculations could wrap around on 32-bit platforms when processing malicious animation files. An attacker could craft a model file with an oversized vertex count to trigger a heap buffer overflow via memcpy. The fix adds bounds checks against `SIZE_MAX` before all size computations used in memory copy operations.

critical

Critical OIDC Cache Key Collision in LiteLLM: Authentication Bypass & Privilege Escalation

LiteLLM versions prior to 1.87.0 contained a critical vulnerability in OIDC userinfo caching that allowed attackers to bypass authentication and escalate privileges through cache key collisions. By upgrading to version 1.87.0, applications eliminate the attack surface that could permit unauthorized users to assume the identity of legitimate authenticated users. This fix is essential for any production system using LiteLLM's OIDC integration.

critical

How buffer overflow in P-256 key decompression happens in C with mbedTLS and how to fix it

A critical buffer overflow vulnerability was discovered in `helpers/src/edhoc_cipher_suite_2.c` within the EDHOC cipher suite 2 implementation. The `mbedtls_ecp_decompress()` function used `raw_key_len` to copy a compressed peer public key into a fixed-size buffer without first verifying that the key length fit within the destination. An attacker sending a crafted EDHOC message with an oversized compressed key could exploit this to corrupt adjacent memory, potentially achieving remote code execu