fix: json_escape SSH key names and fix GCP metadata injection (#958)

SSH key registration in 11 cloud providers used unescaped key_name
directly in JSON request bodies. If the hostname (used to generate
key names) contained JSON-special characters like double-quotes, it
could break out of the JSON string and inject arbitrary JSON fields.

Fix: use json_escape for key_name in all providers, matching the
pattern already used by Scaleway.

Also fix GCP create_server which embedded the startup script inline
in --metadata with comma delimiters. Commas in the script could break
metadata parsing or inject additional metadata keys. Fix: use
--metadata-from-file for the startup script.

Affected providers: Hetzner, DigitalOcean, Vultr, BinaryLane,
Hostinger, Contabo, Cherry, HOSTKEY, Civo, Linode, Genesis Cloud, GCP.

Agent: security-auditor

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-13 09:03:35 -08:00 committed by GitHub
parent fd80f1992c
commit 3c3c697ea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 44 additions and 26 deletions

View file

@ -72,9 +72,10 @@ binarylane_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(binarylane_api POST "/account/keys" "$register_body")

View file

@ -87,9 +87,10 @@ cherry_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"label\":\"$key_name\",\"key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"label\":$json_name,\"key\":$json_pub_key}"
local register_response
register_response=$(cherry_api POST "/ssh-keys" "$register_body")

View file

@ -64,9 +64,10 @@ civo_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(civo_api POST "/sshkeys" "$register_body")

View file

@ -111,9 +111,10 @@ contabo_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"type\":\"ssh\",\"value\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"type\":\"ssh\",\"value\":$json_pub_key}"
local register_response
register_response=$(contabo_api POST "/compute/secrets" "$register_body")

View file

@ -74,9 +74,10 @@ do_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(do_api POST "/account/keys" "$register_body")

View file

@ -108,11 +108,15 @@ create_server() {
log_step "Creating GCP instance '${name}' (type: ${machine_type}, zone: ${zone})..."
local userdata
userdata=$(get_cloud_init_userdata)
local pub_key
pub_key=$(cat "${HOME}/.ssh/id_ed25519.pub")
# Write startup script to a temp file to avoid --metadata comma delimiter issues
local startup_script_file
startup_script_file=$(mktemp)
track_temp_file "${startup_script_file}"
get_cloud_init_userdata > "${startup_script_file}"
local gcloud_err
gcloud_err=$(mktemp)
track_temp_file "${gcloud_err}"
@ -122,7 +126,8 @@ create_server() {
--machine-type="${machine_type}" \
--image-family="${image_family}" \
--image-project="${image_project}" \
--metadata="startup-script=${userdata},ssh-keys=${GCP_USERNAME}:${pub_key}" \
--metadata-from-file="startup-script=${startup_script_file}" \
--metadata="ssh-keys=${GCP_USERNAME}:${pub_key}" \
--project="${GCP_PROJECT}" \
--quiet \
>/dev/null 2>"${gcloud_err}"; then

View file

@ -72,9 +72,10 @@ genesis_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"value\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"value\":$json_pub_key}"
local register_response
register_response=$(genesis_api POST "/ssh-keys" "$register_body")

View file

@ -68,9 +68,10 @@ hetzner_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(hetzner_api POST "/ssh_keys" "$register_body")

View file

@ -74,9 +74,10 @@ hostinger_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(hostinger_api POST "/ssh-keys" "$register_body")

View file

@ -79,10 +79,11 @@ hostkey_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
json_name=$(json_escape "$key_name")
local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}"
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
local register_response
register_response=$(hostkey_api POST "/ssh_keys" "$register_body")
@ -243,7 +244,9 @@ destroy_server() {
log_step "Destroying instance $instance_id..."
local response
response=$(hostkey_api POST "/eq/terminate" "{\"id\":\"$instance_id\"}")
local json_id
json_id=$(json_escape "$instance_id")
response=$(hostkey_api POST "/eq/terminate" "{\"id\":$json_id}")
if echo "$response" | grep -qi "error"; then
log_error "Failed to destroy instance: $response"

View file

@ -71,9 +71,10 @@ linode_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"label\":\"$key_name\",\"ssh_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"label\":$json_name,\"ssh_key\":$json_pub_key}"
local register_response
register_response=$(linode_api POST "/profile/sshkeys" "$register_body")

View file

@ -71,9 +71,10 @@ vultr_register_ssh_key() {
local pub_path="$2"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
local json_pub_key json_name
json_pub_key=$(json_escape "$pub_key")
local register_body="{\"name\":\"$key_name\",\"ssh_key\":$json_pub_key}"
json_name=$(json_escape "$key_name")
local register_body="{\"name\":$json_name,\"ssh_key\":$json_pub_key}"
local register_response
register_response=$(vultr_api POST "/ssh-keys" "$register_body")