2121_ALERT_LIB_LOADED=1
2222
2323# shellcheck disable=SC2034
24- ALERT_LIB_VERSION=" 1.0.5 "
24+ ALERT_LIB_VERSION=" 1.0.6 "
2525
2626# Channel registry — consuming projects populate via alert_channel_register()
2727# Uses parallel indexed arrays instead of declare -A to avoid scope issues
@@ -321,6 +321,17 @@ _alert_build_mime() {
321321_alert_email_local () {
322322 local recip=" $1 " subject=" $2 " text_file=" $3 " html_file=" $4 " format=" ${5:- text} "
323323 local from=" ${ALERT_SMTP_FROM:- root@ $(hostname -f 2>/ dev/ null || hostname)} "
324+ # Defense-in-depth: strip CR/LF from all values used in email headers
325+ recip=" ${recip// $' \r ' / } "
326+ recip=" ${recip// $' \n ' / } "
327+ subject=" ${subject// $' \r ' / } "
328+ subject=" ${subject// $' \n ' / } "
329+ from=" ${from// $' \r ' / } "
330+ from=" ${from// $' \n ' / } "
331+ local reply_to=" ${ALERT_EMAIL_REPLY_TO:- } "
332+ reply_to=" ${reply_to// $' \r ' / } "
333+ reply_to=" ${reply_to// $' \n ' / } "
334+
324335 local sendmail_bin mail_bin
325336 sendmail_bin=$( command -v sendmail 2> /dev/null || true)
326337 mail_bin=$( command -v mail 2> /dev/null || true)
@@ -339,8 +350,8 @@ _alert_email_local() {
339350 echo " From: $from "
340351 echo " To: $recip "
341352 echo " Subject: $subject "
342- if [ -n " ${ALERT_EMAIL_REPLY_TO :- } " ]; then
343- echo " Reply-To: $ALERT_EMAIL_REPLY_TO "
353+ if [ -n " $reply_to " ]; then
354+ echo " Reply-To: $reply_to "
344355 fi
345356 echo " "
346357 cat " $text_file "
@@ -359,8 +370,8 @@ _alert_email_local() {
359370 echo " From: $from "
360371 echo " To: $recip "
361372 echo " Subject: $subject "
362- if [ -n " ${ALERT_EMAIL_REPLY_TO :- } " ]; then
363- echo " Reply-To: $ALERT_EMAIL_REPLY_TO "
373+ if [ -n " $reply_to " ]; then
374+ echo " Reply-To: $reply_to "
364375 fi
365376 echo " Content-Type: text/html; charset=UTF-8"
366377 echo " Content-Transfer-Encoding: base64"
@@ -388,8 +399,8 @@ _alert_email_local() {
388399 echo " From: $from "
389400 echo " To: $recip "
390401 echo " Subject: $subject "
391- if [ -n " ${ALERT_EMAIL_REPLY_TO :- } " ]; then
392- echo " Reply-To: $ALERT_EMAIL_REPLY_TO "
402+ if [ -n " $reply_to " ]; then
403+ echo " Reply-To: $reply_to "
393404 fi
394405 _alert_build_mime " $text_body " " $html_body "
395406 } | " $sendmail_bin " -t -oi
@@ -491,21 +502,25 @@ _alert_deliver_email() {
491502 if [ -n " ${ALERT_SMTP_RELAY:- } " ]; then
492503 # relay path: always build full multipart MIME message
493504 local from=" ${ALERT_SMTP_FROM:- root@ $(hostname -f 2>/ dev/ null || hostname)} "
505+ # Defense-in-depth: strip CR/LF from all header values to prevent injection
506+ recip=" ${recip// $' \r ' / } "
507+ recip=" ${recip// $' \n ' / } "
508+ from=" ${from// $' \r ' / } "
509+ from=" ${from// $' \n ' / } "
510+ local reply_to=" ${ALERT_EMAIL_REPLY_TO:- } "
511+ reply_to=" ${reply_to// $' \r ' / } "
512+ reply_to=" ${reply_to// $' \n ' / } "
494513 local text_body html_body
495514 text_body=$( cat " $text_file " )
496- if [ -n " $html_file " ] && [ -f " $html_file " ]; then
497- html_body=$( cat " $html_file " )
498- else
499- html_body=" "
500- fi
515+ html_body=$( cat " $html_file " )
501516 local msg_file
502517 msg_file=$( mktemp " ${ALERT_TMPDIR} /alert_relay_msg.XXXXXX" )
503518 {
504519 echo " From: $from "
505520 echo " To: $recip "
506521 echo " Subject: $subject "
507- if [ -n " ${ALERT_EMAIL_REPLY_TO :- } " ]; then
508- echo " Reply-To: $ALERT_EMAIL_REPLY_TO "
522+ if [ -n " $reply_to " ]; then
523+ echo " Reply-To: $reply_to "
509524 fi
510525 echo " Date: $( date -R 2> /dev/null || date) "
511526 _alert_build_mime " $text_body " " $html_body "
@@ -581,9 +596,10 @@ _alert_slack_post_message() {
581596 # Inject "channel" field after opening brace
582597 # Uses awk instead of sed to avoid delimiter collision if channel
583598 # contains / or & (sed s/// treats both as special characters)
584- local modified_payload
599+ local modified_payload escaped_channel
585600 modified_payload=$( mktemp " ${ALERT_TMPDIR} /alert_slack_msg.XXXXXX" )
586- ch=" $channel " awk ' NR==1 && /^\{/ { print "{\"channel\":\"" ENVIRON["ch"] "\"," substr($0,2); next } { print }' \
601+ escaped_channel=$( _alert_json_escape " $channel " )
602+ ch=" $escaped_channel " awk ' NR==1 && /^\{/ { print "{\"channel\":\"" ENVIRON["ch"] "\"," substr($0,2); next } { print }' \
587603 " $payload_file " > " $modified_payload "
588604 # Write token to config file — keeps it out of /proc/PID/cmdline
589605 local auth_cfg
@@ -638,7 +654,7 @@ _alert_slack_upload() {
638654 local url_response upload_url file_id
639655 url_response=$( _alert_curl_post " https://slack.com/api/files.getUploadURLExternal" \
640656 -K " $auth_cfg " \
641- -d " filename=$filename " \
657+ --data-urlencode " filename=$filename " \
642658 -d " length=$fsize " ) || { command rm -f " $auth_cfg " ; return 1; }
643659 case " $url_response " in
644660 * ' "ok":true' * ) ;;
@@ -653,6 +669,18 @@ _alert_slack_upload() {
653669 upload_url=$( printf ' %s' " $url_response " | sed -n ' s/.*"upload_url" *: *"\([^"]*\)".*/\1/p' )
654670 file_id=$( printf ' %s' " $url_response " | sed -n ' s/.*"file_id" *: *"\([^"]*\)".*/\1/p' )
655671
672+ # Validate upload URL is HTTPS (defense-in-depth: reject http, file, ftp, etc.)
673+ # Case-insensitive match: MITM attacker could use any casing (${var,,} prohibited on bash 4.1)
674+ case " $upload_url " in
675+ [hH][tT][tT][pP][sS]://* )
676+ ;;
677+ * )
678+ echo " alert_lib: Slack upload URL rejected (not https): ${upload_url:- (empty)} " >&2
679+ command rm -f " $auth_cfg "
680+ return 1
681+ ;;
682+ esac
683+
656684 # Step 2: upload file content to presigned URL (no auth header needed)
657685 _alert_curl_post " $upload_url " -F " file=@$file_path " > /dev/null || {
658686 echo " alert_lib: Slack file upload to presigned URL failed." >&2
0 commit comments