Skip to content

Security: arcup --self-update replaces the installer without authenticity verification #204

Description

@mssystem1

Summary

The arcup --self-update command downloads the latest installer directly from the mutable main branch and replaces the local executable without verifying a checksum, commit SHA, or cryptographic signature.

The downloaded file is only checked for an ARCUP_INSTALLER_VERSION declaration. A file containing a higher version string is accepted and copied over the installed arcup executable.

This differs from Arc node release installation, where the archive is verified against a downloaded SHA-256 checksum.

Repository

circlefin/arc-node

Create issue:

https://github.com/circlefin/arc-node/issues/new

Affected file

arcup/arcup

Mutable self-update source:

arc-node/arcup/arcup

Lines 23 to 25 in 745ba4e

ARCUP_BIN_PATH="$BIN_DIR/arcup"
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com}"
case "$GITHUB_API_URL" in

Self-update implementation:

arc-node/arcup/arcup

Lines 435 to 469 in 745ba4e

require_command curl "curl not found. Please install curl."
_ARCUP_TMP_FILE=$(mktemp)
info "Downloading latest arcup..."
if ! curl_with_headers -fsSL -o "$_ARCUP_TMP_FILE" "$ARCUP_BIN_URL"; then
error "Failed to download arcup update"
fi
local remote_version
remote_version=$(grep '^ARCUP_INSTALLER_VERSION=' "$_ARCUP_TMP_FILE" | sed -E 's/ARCUP_INSTALLER_VERSION="(.+)"/\1/')
if [[ -z "$remote_version" ]]; then
error "Failed to determine remote version"
fi
if ! version_gt "$remote_version" "$ARCUP_INSTALLER_VERSION"; then
info "Arcup is already up to date (version $ARCUP_INSTALLER_VERSION)"
exit 0
fi
info "Updating from $ARCUP_INSTALLER_VERSION to $remote_version..."
if cp "$_ARCUP_TMP_FILE" "$ARCUP_BIN_PATH" 2>/dev/null; then
chmod 755 "$ARCUP_BIN_PATH"
else
error "Failed to update arcup at '$ARCUP_BIN_PATH' — ensure it is writable"
fi
echo ""
info "Arcup updated successfully to version $remote_version"
exit 0
}
# Remove arc binaries and env files

The update path downloads the script and installs it with:

curl_with_headers -fsSL -o "$_ARCUP_TMP_FILE" "$ARCUP_BIN_URL"

followed by:

cp "$_ARCUP_TMP_FILE" "$ARCUP_BIN_PATH"
chmod 755 "$ARCUP_BIN_PATH"

Steps to reproduce

  1. Install an older version of arcup.

  2. Run:

    arcup --self-update
  3. Observe that the update is downloaded from:

    https://raw.githubusercontent.com/circlefin/arc-node/main/arcup/arcup
    
  4. Observe that the downloaded file is only inspected for:

    ARCUP_INSTALLER_VERSION="..."
  5. No SHA-256 checksum, pinned commit, release asset signature, or trusted signing key is verified before the file replaces the installed executable.

A minimal file containing a higher version declaration and arbitrary shell commands would satisfy the current version check.

Expected behavior

A self-updating executable should verify that the downloaded replacement was published by the Arc maintainers before executing or installing it.

The update should be obtained from an immutable release asset and verified using at least one trusted mechanism, such as:

  • a checksum embedded in or anchored by the currently installed updater;
  • a cryptographic signature checked against a pinned maintainer key;
  • a pinned release commit or immutable GitHub release asset with verified provenance.

Actual behavior

arcup trusts the contents returned from a mutable main branch URL and installs them after checking only that the file declares a newer version.

The existing GPG/checksum verification used for Arc node archives is not applied to the self-update path.

Impact

If the update source or delivery path is compromised, the next user running:

arcup --self-update

could install and subsequently execute attacker-controlled shell code.

Depending on how Arc operators run the installer, this could expose:

  • validator or node host credentials;
  • environment variables;
  • filesystem contents accessible to the operator;
  • node configuration and secrets;
  • the integrity of the Arc node installation.

This is particularly sensitive because installer scripts are commonly executed with elevated filesystem permissions.

Suggested fix

Publish arcup as an immutable GitHub release asset and verify it before replacement.

For example:

  1. Download arcup, arcup.sha256, and optionally arcup.asc from the same tagged release.
  2. Verify the checksum.
  3. Verify the signature against a pinned Arc release-signing fingerprint.
  4. Validate the downloaded script before replacing the current executable.
  5. Perform the replacement atomically.

Example flow:

download_file "$tag" "arcup" "$TMP_DIR"
download_file "$tag" "arcup.sha256" "$TMP_DIR"

verify_checksum_file \
  "$TMP_DIR/arcup" \
  "$TMP_DIR/arcup.sha256" \
  "arcup"

install -m 755 "$TMP_DIR/arcup" "$ARCUP_BIN_PATH.new"
mv -f "$ARCUP_BIN_PATH.new" "$ARCUP_BIN_PATH"

The updater should fail closed when verification cannot be completed.

Tests

Add tests confirming that:

  • a self-update with a mismatched checksum is rejected;
  • an unsigned update is rejected when signatures are required;
  • a downloaded file with only a higher version string is insufficient;
  • the current executable is preserved when verification fails;
  • updates are obtained from an immutable release reference;
  • replacement is atomic.

Checklist

  • Move self-update artifacts to immutable GitHub releases
  • Add SHA-256 verification to self-update
  • Configure and pin the Arc release-signing key
  • Fail closed when authenticity cannot be verified
  • Replace the executable atomically
  • Add self-update integrity tests
  • Document the update trust model

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions