From f0c97f281c63b5c49996ac4e0adabafeeb08af4c Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 27 Apr 2026 00:22:56 +0100 Subject: [PATCH 1/3] prevent secret leaking via headers, prevent symlink upload and git directory upload --- scripts/ipfs-pin.sh | 61 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/scripts/ipfs-pin.sh b/scripts/ipfs-pin.sh index e5bd4e2..10b01fa 100755 --- a/scripts/ipfs-pin.sh +++ b/scripts/ipfs-pin.sh @@ -42,6 +42,31 @@ if ! command -v ipfs >/dev/null 2>&1; then exit 1 fi +# Auth-header tmp file plumbing. We pass the API key to curl via `-H @file` +# (header-file syntax, curl >= 7.55) so the secret never appears in this +# process's argv, where it would otherwise be visible to any user on the +# system via `ps`. Files are mktemp'd with mode 600 and removed on exit. +SECRET_TMP_FILES=() +cleanup_secret_files() { + local f + for f in "${SECRET_TMP_FILES[@]:-}"; do + [ -n "$f" ] && [ -f "$f" ] && rm -f "$f" + done +} +trap cleanup_secret_files EXIT INT TERM + +# Create a 0600 tmp file containing one HTTP header line and emit its path on +# stdout. The caller passes that path to `curl -H @path`. Track it for cleanup. +write_auth_header_file() { + local header_line="$1" + local f + f=$(mktemp "${TMPDIR:-/tmp}/ipfs-pin-auth.XXXXXX") + chmod 600 "$f" + printf '%s\n' "$header_line" > "$f" + SECRET_TMP_FILES+=("$f") + printf '%s' "$f" +} + # Usage message usage() { local col=50 @@ -210,8 +235,9 @@ pin_single_file() { fi echo -e "${CYAN}Uploading file to Pinata service...${NC}" + pinata_auth_file=$(write_auth_header_file "Authorization: Bearer ${PINATA_API_KEY}") response=$(curl -s -X POST "https://uploads.pinata.cloud/v3/files" \ - -H "Authorization: Bearer ${PINATA_API_KEY}" \ + -H "@${pinata_auth_file}" \ -F "file=@$file" \ -F "network=public" \ ) @@ -240,8 +266,9 @@ pin_single_file() { fi echo -e "${CYAN}Uploading file to Blockfrost service...${NC}" + blockfrost_auth_file=$(write_auth_header_file "project_id: $BLOCKFROST_API_KEY") response=$(curl -s -X POST "https://ipfs.blockfrost.io/api/v0/ipfs/add" \ - -H "project_id: $BLOCKFROST_API_KEY" \ + -H "@${blockfrost_auth_file}" \ -F "file=@$file" \ ) # Check response for errors @@ -278,10 +305,11 @@ pin_single_file() { base64_content=$(base64 -i "$file") echo -e "${CYAN}Uploading file to NMKR service...${NC}" + nmkr_auth_file=$(write_auth_header_file "Authorization: Bearer ${NMKR_API_KEY}") response=$(curl -s -X POST "https://studio-api.nmkr.io/v2/UploadToIpfs/${NMKR_USER_ID}" \ -H 'accept: text/plain' \ -H 'Content-Type: application/json' \ - -H "Authorization: Bearer ${NMKR_API_KEY}" \ + -H "@${nmkr_auth_file}" \ -d @- <&2 + exit 1 + fi echo -e " " echo -e "${CYAN}Processing single file: ${YELLOW}$input_path${NC}" pin_single_file "$input_path" From 6f579c5fdf9c4e3ed02df25db8501b8d7cd7a505 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 27 Apr 2026 00:29:43 +0100 Subject: [PATCH 2/3] make it much harder to pin secret files --- README.md | 2 +- scripts/ipfs-pin.sh | 62 +++++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1f4f287..fd96ae6 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ This repository holds shell scripts that Intersect uses to engage in Cardano on- - Checks if a file is accessible via free IPFS gateways - [ipfs-pin.sh](./scripts/ipfs-pin.sh) - Pins file(s) across local IPFS node, Pinata, Blockfrost, and NMKR (enabled by default; disable individually with `--no-local`, `--no-pinata`, `--no-blockfrost`, `--no-nmkr`) - - Accepts a single file or a directory; use `--just-jsonld` to limit a directory walk to `.jsonld` files + - Accepts a single file by default. To recursively pin a directory you must explicitly pass `--directory` (think of it as the equivalent of `rm -r`); without it, a directory path is rejected to prevent accidental bulk uploads. Combine with `--just-jsonld` to limit the walk to `.jsonld` files. The walk skips `.git`/`.svn`/`.hg`, symlinks, and any file whose name matches a sensitive pattern (`*.skey`, `*.vkey`, `.env*`, `id_rsa*`, `*.pem`, `*.p12`, `*.pfx`). - `--check-too` first runs `ipfs-check.sh` and skips pinning if the file is already discoverable #### CIP-108 Scripts diff --git a/scripts/ipfs-pin.sh b/scripts/ipfs-pin.sh index 10b01fa..878732b 100755 --- a/scripts/ipfs-pin.sh +++ b/scripts/ipfs-pin.sh @@ -72,9 +72,10 @@ usage() { local col=50 echo -e "${UNDERLINE}${BOLD}Pin files to local IPFS node and via Blockfrost, NMKR and Pinata${NC}" echo -e "\n" - echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--check-too${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" + echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--directory${NC}] [${GREEN}--check-too${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" printf "Params: ${GREEN}%-*s${GRAY}%s${NC}\n" $((col-8)) "" "- Path to your file or directory containing files" - printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If a directory is provided, only .jsonld files will be processed (default: $JUST_JSONLD)" + printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--directory]" "- REQUIRED if the path is a directory. Without this flag, a directory path is rejected to prevent accidental bulk uploads (e.g. pointing at a project root and pinning every file in it). Treat this as the equivalent of rm's '-r'." + printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If --directory is set, only .jsonld files will be processed (default: $JUST_JSONLD)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--check-too]" "- Run a check if file is discoverable on ipfs, only pin if not discoverable (default: $CHECK_TOO)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-local]" "- Don't try to pin file on local ipfs node (default: $DEFAULT_HOST_ON_LOCAL_NODE)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-pinata]" "- Don't try to pin file on pinata service (default: $DEFAULT_HOST_ON_PINATA)" @@ -88,6 +89,7 @@ usage() { input_path="" check_discoverable="$CHECK_TOO" just_jsonld="$JUST_JSONLD" +allow_directory="false" local_host="$DEFAULT_HOST_ON_LOCAL_NODE" pinata_host="$DEFAULT_HOST_ON_PINATA" blockfrost_host="$DEFAULT_HOST_ON_BLOCKFROST" @@ -100,6 +102,10 @@ while [[ $# -gt 0 ]]; do check_discoverable="true" shift ;; + --directory) + allow_directory="true" + shift + ;; --just-jsonld) just_jsonld="true" shift @@ -176,13 +182,29 @@ if [ "$nmkr_host" = "true" ]; then fi fi +# Refuse to pin obviously-secret files. Public-IPFS pinning a Cardano signing +# key (.skey) or an .env credentials file would publish it permanently +SENSITIVE_BASENAME_REGEX='\.(skey|vkey|env)$|^\.env(\..*)?$|^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$|\.(pem|p12|pfx)$' +is_sensitive_filename() { + local name + name=$(basename "$1") + [[ "$name" =~ $SENSITIVE_BASENAME_REGEX ]] +} + # Function to pin a single file pin_single_file() { local file="$1" - + + if is_sensitive_filename "$file"; then + echo -e " " + echo -e "${RED}Refusing to pin '${YELLOW}$file${RED}': filename matches a sensitive pattern (signing key / env / private key / cert). Pinning would publish it permanently to public IPFS.${NC}" >&2 + echo -e "${GRAY}If you genuinely intend to pin this file, rename it first or remove it from the directory tree.${NC}" >&2 + return 1 + fi + echo -e " " echo -e "${CYAN}Processing file: ${YELLOW}$file${NC}" - + # Generate CID from the given file echo -e "${CYAN}Generating CID for the file...${NC}" @@ -306,18 +328,17 @@ pin_single_file() { echo -e "${CYAN}Uploading file to NMKR service...${NC}" nmkr_auth_file=$(write_auth_header_file "Authorization: Bearer ${NMKR_API_KEY}") + # Build the JSON body with jq so the basename and base64 payload are + # properly escaped + nmkr_body=$(jq -n \ + --arg b64 "$base64_content" \ + --arg name "$(basename "$file")" \ + '{fileFromBase64: $b64, name: $name, mimetype: "application/json"}') response=$(curl -s -X POST "https://studio-api.nmkr.io/v2/UploadToIpfs/${NMKR_USER_ID}" \ -H 'accept: text/plain' \ -H 'Content-Type: application/json' \ -H "@${nmkr_auth_file}" \ - -d @- <&2 @@ -337,8 +358,15 @@ EOF # Main processing logic if [ -d "$input_path" ]; then - # If input is a directory: pin files (optionally only .jsonld files) including subdirectories + # Directory uploads must be opted into explicitly + if [ "$allow_directory" != "true" ]; then + echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a directory.${NC}" >&2 + echo -e "${YELLOW}Pass ${GREEN}--directory${YELLOW} to confirm you want to recursively pin its contents (think of this as the equivalent of ${GREEN}rm -r${YELLOW} — it will publish every regular non-symlink, non-VCS, non-sensitive file under the tree to public IPFS, irreversibly).${NC}" >&2 + echo -e "${GRAY}Combine with ${GREEN}--just-jsonld${GRAY} to limit the walk to *.jsonld files.${NC}" >&2 + exit 1 + fi echo -e " " + echo -e "${YELLOW}Warning: ${GREEN}--directory${YELLOW} is set — this will recursively pin files under '${BRIGHTWHITE}$input_path${YELLOW}' to every enabled pinning service. Pinning is publishing: anything uploaded becomes permanently retrievable from public IPFS gateways.${NC}" >&2 echo -e "${CYAN}Processing directory: ${YELLOW}$input_path${NC}" # Pruning rules for the recursive walk: @@ -395,9 +423,11 @@ if [ -d "$input_path" ]; then echo -e "${GREEN}All files processed successfully!${NC}" elif [ -f "$input_path" ]; then - # Input is a single file. Reject symlinks: `[ -f X ]` follows the link, so - # a `bash ipfs-pin.sh symlink-to-secret.jsonld` would otherwise silently - # upload the target (e.g. /etc/passwd, ~/.cardano/keys/...) to public IPFS. + if [ "$allow_directory" = "true" ]; then + echo -e "${RED}Error: ${GREEN}--directory${RED} was set, but '${YELLOW}$input_path${RED}' is a single file. Drop ${GREEN}--directory${RED} for single-file uploads.${NC}" >&2 + exit 1 + fi + # Input is a single file. Reject symlinks: `[ -f X ]` follows the link. if [ -L "$input_path" ]; then echo -e "${RED}Error: '${YELLOW}$input_path${RED}' is a symbolic link. Refusing to pin a symlink target — pass the real file path instead.${NC}" >&2 exit 1 From 21c1fca6f598e285b95d1bebae0d7adc585266a3 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 27 Apr 2026 00:32:14 +0100 Subject: [PATCH 3/3] remove the check-too option --- README.md | 1 - scripts/ipfs-pin.sh | 28 +++------------------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index fd96ae6..458bcc0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ This repository holds shell scripts that Intersect uses to engage in Cardano on- - [ipfs-pin.sh](./scripts/ipfs-pin.sh) - Pins file(s) across local IPFS node, Pinata, Blockfrost, and NMKR (enabled by default; disable individually with `--no-local`, `--no-pinata`, `--no-blockfrost`, `--no-nmkr`) - Accepts a single file by default. To recursively pin a directory you must explicitly pass `--directory` (think of it as the equivalent of `rm -r`); without it, a directory path is rejected to prevent accidental bulk uploads. Combine with `--just-jsonld` to limit the walk to `.jsonld` files. The walk skips `.git`/`.svn`/`.hg`, symlinks, and any file whose name matches a sensitive pattern (`*.skey`, `*.vkey`, `.env*`, `id_rsa*`, `*.pem`, `*.p12`, `*.pfx`). - - `--check-too` first runs `ipfs-check.sh` and skips pinning if the file is already discoverable #### CIP-108 Scripts diff --git a/scripts/ipfs-pin.sh b/scripts/ipfs-pin.sh index 878732b..30c3f68 100755 --- a/scripts/ipfs-pin.sh +++ b/scripts/ipfs-pin.sh @@ -4,8 +4,6 @@ # Can change if you want! -# Default behavior is to not check if file is discoverable on IPFS -CHECK_TOO="false" JUST_JSONLD="false" # Pinning services to host the file on IPFS @@ -72,11 +70,10 @@ usage() { local col=50 echo -e "${UNDERLINE}${BOLD}Pin files to local IPFS node and via Blockfrost, NMKR and Pinata${NC}" echo -e "\n" - echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--directory${NC}] [${GREEN}--check-too${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" + echo -e "Syntax:${BOLD} $0 ${GREEN}${NC} [${GREEN}--directory${NC}] [${GREEN}--no-local${NC}] [${GREEN}--no-pinata${NC}] [${GREEN}--no-blockfrost${NC}] [${GREEN}--no-nmkr${NC}]" printf "Params: ${GREEN}%-*s${GRAY}%s${NC}\n" $((col-8)) "" "- Path to your file or directory containing files" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--directory]" "- REQUIRED if the path is a directory. Without this flag, a directory path is rejected to prevent accidental bulk uploads (e.g. pointing at a project root and pinning every file in it). Treat this as the equivalent of rm's '-r'." printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--just-jsonld]" "- If --directory is set, only .jsonld files will be processed (default: $JUST_JSONLD)" - printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--check-too]" "- Run a check if file is discoverable on ipfs, only pin if not discoverable (default: $CHECK_TOO)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-local]" "- Don't try to pin file on local ipfs node (default: $DEFAULT_HOST_ON_LOCAL_NODE)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-pinata]" "- Don't try to pin file on pinata service (default: $DEFAULT_HOST_ON_PINATA)" printf " ${GREEN}%-*s${NC}${GRAY}%s${NC}\n" $((col-8)) "[--no-blockfrost]" "- Don't try to pin file on blockfrost service (default: $DEFAULT_HOST_ON_BLOCKFROST)" @@ -87,7 +84,6 @@ usage() { # Initialize variables with defaults input_path="" -check_discoverable="$CHECK_TOO" just_jsonld="$JUST_JSONLD" allow_directory="false" local_host="$DEFAULT_HOST_ON_LOCAL_NODE" @@ -98,10 +94,6 @@ nmkr_host="$DEFAULT_HOST_ON_NMKR" # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in - --check-too) - check_discoverable="true" - shift - ;; --directory) allow_directory="true" shift @@ -212,23 +204,9 @@ pin_single_file() { # use CIDv1 ipfs_cid=$(ipfs add -Q --cid-version 1 "$file") echo -e "CID: ${YELLOW}$ipfs_cid${NC}" - - # If user wants to check if file is discoverable on IPFS - if [ "$check_discoverable" = "true" ]; then - echo -e "${CYAN}Using ./scripts/ipfs-check.sh script to check if file is discoverable on IPFS...${NC}" - # check if file is discoverable on IPFS - if ! ./scripts/ipfs-check.sh "$file"; then - echo -e "${YELLOW}File is not discoverable on IPFS. Proceeding to pin it.${NC}" - else - echo -e "${GREEN}File is already discoverable on IPFS. No need to pin it.${NC}" - return 0 - fi - else - echo -e "${CYAN}Skipping check of file on ipfs...${NC}" - fi - + echo -e " " - echo -e "${CYAN}File is not hosted on IPFS, so pinning it...${NC}" + echo -e "${CYAN}Pinning file to enabled services...${NC}" # Pin on local node if [ "$local_host" = "true" ]; then