كيفية البرمجة بأمان في باش

لماذا باش؟


هناك صفائف ووضع آمن في باش. عند استخدامه بشكل صحيح ، يكون bash متسقًا تقريبًا مع ممارسات الترميز الآمنة.

من الصعب ارتكاب خطأ في الأسماك ، ولكن لا يوجد وضع آمن. لذلك ، يجب أن تكون النماذج الأولية في الأسماك ثم الترجمة من الأسماك إلى الباش فكرة جيدة إذا كنت تعرف كيفية القيام بذلك بشكل صحيح.

مقدمة


يرافق هذا الدليل ShellHarden ، لكن المؤلف يوصي أيضًا بـ ShellCheck بحيث لا تختلف قواعد ShellHarden عن ShellCheck.

إن باش ليست لغة حيث أن الطريقة الصحيحة لحل مشكلة في نفس الوقت هي الأسهل . إذا أجريت اختبار bash الآمن للبرمجة ، فستكون القاعدة الأولى في BashPitfalls هي: استخدام علامات الاقتباس دائمًا.

الشيء الرئيسي الذي تحتاج إلى معرفته عن البرمجة في باش


علامات الاقتباس الهوسي! يجب اعتبار المتغير غير المقتبس قنبلة مفخخة: ينفجر عند التلامس مع الفضاء. نعم ، ينفجر بمعنى تقسيم سلسلة إلى صفيف . على وجه الخصوص ، يتم تقسيم الامتدادات المتغيرة مثل $var واستبدالات الأوامر مثل $(cmd) عندما يتم توسيع السلسلة الداخلية إلى مصفوفة بسبب الانقسام في متغير $IFS الخاص بمساحة افتراضية. عادة ما يكون هذا غير مرئي ، لأنه في الغالب تكون النتيجة صفيفًا من عنصر واحد ، لا يمكن تمييزه عن السلسلة المتوقعة.

ليس هذا فقط موسعًا ، ولكن أيضًا أحرف البدل ( *? ). تحدث هذه العملية بعد تقسيم الكلمة ، لذلك إذا كانت الكلمة تحتوي على حرف بدل واحد على الأقل ، تتحول الكلمة إلى حرف بدل ينطبق على أي مسارات ملفات مناسبة. لذلك تبدأ هذه الميزة للتطبيق على نظام الملفات!

يمنع عرض الأسعار تقسيم الكلمات وتوسيع النمط للمتغيرات واستبدال الأوامر.

ملحق متغير:

  • جيد: "$my_var"
  • سيئ: $my_var

استبدال الأمر:

  • جيد: "$(cmd)"
  • سيء: $(cmd)

هناك استثناءات بعلامات اقتباس اختيارية ، لكن علامات الاقتباس لن تؤذي أبدًا ، والقاعدة العامة هي توخي الحذر لعدم اقتباس المتغيرات غير المسعرة ، لذلك لن نبحث عن استثناءات الحدود لصالحك. يبدو خاطئًا ، والممارسة الخاطئة منتشرة بشكل كافٍ لإثارة الشك: تم كتابة العديد من النصوص مع معالجة مكسورة لأسماء الملفات والمسافات فيها ...

يذكر ShellHarden استثناءات قليلة فقط - هل هذه المتغيرات ذات محتويات رقمية مثل $? ، $# و ${#array[@]} .

هل أحتاج إلى استخدام backticks؟


يمكن أن تحتوي بدائل الأوامر أيضًا على الشكل التالي:

  • صحيح: "`cmd`"
  • سيئ: `cmd`

على الرغم من أنه يمكن استخدام هذا النمط بشكل صحيح ، إلا أنه يبدو أقل ملاءمة في علامات الاقتباس وأقل قابلية للقراءة عند تداخله. الإجماع هنا واضح جدا: تجنبه.

تعيد ShellHarden كتابة هذه العلامات بين قوسين بالدولار.

هل يجب استخدام الأقواس المتعرجة؟


يتم استخدام الأقواس لإقحام الأوتار ، لذلك عادة ما تكون زائدة عن الحاجة:

  • تالف: some_command $arg1 $arg2 $arg3
  • ضعيف ومطوّل: some_command ${arg1} ${arg2} ${arg3}
  • جيد ، ولكن مطوّل: some_command "${arg1}" "${arg2}" "${arg3}"
  • جيد: some_command "$arg1" "$arg2" "$arg3"

من الناحية النظرية ، لا يعد استخدام الأقواس المتعرجة دائمًا مشكلة ، ولكن وفقًا لتجربة مؤلفك ، هناك ارتباط سلبي قوي بين الاستخدام غير الضروري للأقواس المتعرجة والاستخدام الصحيح لعلامات الاقتباس - فالجميع تقريبًا يختارون "السيئة والقيمة المطولة" بدلاً من النموذج "الجيد ولكن المطول"!

نظريات مؤلفك:

  • بسبب الخوف من فعل شيء خاطئ: بدلاً من الخطر الحقيقي (نقص علامات الاقتباس) ، قد يقلق المبتدئون من أن متغير $prefix سيتسبب في "$prefix_postfix" متغير "$prefix_postfix" ، ولكنه لا يعمل بهذه الطريقة.
  • عبادة البضائع: كتابة الكود في عهد الخوف الخاطئ الذي سبقه.
  • تتنافس الأقواس مع علامات الاقتباس للحد المسموح به من الإسهاب.

لذلك ، تقرر حظر الأقواس المتعرجة غير الضرورية: ShellHarden يستبدل هذه الخيارات بأبسط شكل جيد.

والآن حول استيفاء السلسلة ، حيث تكون الأقواس المتعرجة مفيدة حقًا:

  • تالف (سلسلة): $var1"more string content"$var2
  • جيد (تسلسل): "$var1""more string content""$var2"
  • جيد (الاستيفاء): "${var1}more string content${var2}"

التسلسل والاستيفاء في باش مكافئ حتى في المصفوفات (وهو أمر مثير للسخرية).

نظرًا لأن ShellHarden لا يقوم بتنسيق الأشكال ، فلا يفترض تغيير الرمز الصحيح. ينطبق هذا على خيار "الاستيفاء" الجيد: من وجهة نظر ShellHarden ، سيكون هذا هو الشكل الصحيح القانوني.

تقوم ShellHarden الآن بإضافة الأقواس المتعرجة وإزالتها حسب الحاجة: في مثال سيئ ، يتم توفير var1 بأقواس ، ولكن لا يُسمح باستخدامها لـ var2 حتى في حالة "good (interpolation)" ، نظرًا لعدم الحاجة إليها في نهاية السطر. يمكن عكس الشرط الأخير بشكل جيد.

Gotcha: الحجج المرقمة


على عكس أسماء معرفات المتغيرات العادية (في التعبير العادي: [_a-zA-Z][_a-zA-Z0-9]* ) ، تتطلب الوسيطات المرقمة أقواس (لا يستكمل الاستيفاء). يقول ShellCheck:

 echo "$10" ^-- SC1037: Braces are required for positionals over 9, eg ${10}. 

يرفض ShellHarden إصلاحه (يعتبر الفرق خفيًا جدًا).

بما أن الأقواس مسموح بها حتى 9 ، فإن ShellHarden يسمح بها لجميع الوسائط المرقمة.

استخدام المصفوفات


لتكون قادرًا على اقتباس جميع المتغيرات ، يجب عليك استخدام صفائف حقيقية ، وليس سلاسل ضخمة زائفة مفصولة بمسافات.

بناء الجملة مطوّل ، ولكن يجب عليك التعامل معه. هذا bashism هو سبب واحد فقط للتخلي عن توافق POSIX لمعظم البرامج النصية الصدفة.

جيد:

 array=( a b ) array+=(c) if [ ${#array[@]} -gt 0 ]; then rm -- "${array[@]}" fi 

سيء:

 pseudoarray=" \ a \ b \ " pseudoarray="$pseudoarray c" if ! [ "$pseudoarray" = '' ]; then rm -- $pseudoarray fi 

هذا هو السبب في أن المصفوفات هي وظيفة أساسية للقشرة: الحجج من الأوامر هي في الأساس صفائف (ونصوص القشرة هي أوامر وحجج). يمكننا أن نقول أن الصدفة ، التي تجعل من المستحيل مصطنعًا تمرير العديد من الحجج ، ستكون كوميديا ​​ولا قيمة لها. بعض الأصداف الشائعة من هذه الفئة تشمل Dash و Busybox Ash. هذه هي الحد الأدنى من الأصداف المتوافقة مع POSIX - ولكن ما الجيد في التوافق إذا لم تكن أهم الأشياء موجودة في POSIX؟

حالات استثنائية عندما تكون حقاً في كسر الخط


مثال مع \v كفاصل بيانات (لاحظ التكرار الثاني):

 IFS=$'\v' read -d '' -ra a < <(printf '%s\v' "$s") || true 

بهذه الطريقة نتجنب توسيع القالب ، وتعمل الطريقة حتى إذا كان فاصل البيانات \n . يحمي التكرار الثاني لفاصل البيانات العنصر الأخير إذا تبين أنه مسافة. لسبب ما ، يجب أن يذهب الخيار -d أولاً ، لذا -rad '' الخيارات في -rad '' مغر ، لكنه لن يعمل. نظرًا لأن القراءة تعرض قيمة غير صفرية في هذه الحالة ، فيجب حمايتها من errexit ( || true ) ، إذا تم تمكينها. تم الاختبار في باش 4.0 و 4.1 و 4.2 و 4.3 و 4.4.

بديل لباش 4.4:

 readarray -td $'\v' a < <(printf '%s\v' "$s") 

أين تبدأ نص باش


من شيء مثل هذا:

 #!/usr/bin/env bash if test "$BASH" = "" || "$BASH" -uc "a=();true \"\${a[@]}\"" 2>/dev/null; then # Bash 4.4, Zsh set -euo pipefail else # Bash 4.3 and older chokes on empty arrays with set -u. set -eo pipefail fi shopt -s nullglob globstar 

وهذا يشمل:

  • شيبانج:
    • قضايا النقل: ربما env المسار المطلق إلى env أفضل لإمكانية النقل من المسار المطلق bash . يمكنك إلقاء نظرة على مثال NixOS . يتطلب POSIX ENV ، ولكن ليس باش.
    • قضايا أمنية: لعدم وجود لغة ، لن يتم قبول خيارات مثل -euo pipefail ! يصبح هذا مستحيلًا عند استخدام إعادة توجيه env ، ولكن حتى إذا كان shebang يبدأ بـ #!/bin/bash ، فهذا ليس هو المكان المناسب للمعلمات التي تؤثر على قيمة البرنامج النصي ، لأنه يمكن تجاوزها ، مما سيجعل من الممكن تنفيذ البرنامج النصي بشكل غير صحيح. ومع ذلك ، كمكافأة ، يمكن إعادة تحديد الخيارات التي لا تؤثر على قيمة البرنامج النصي ، مثل set -x ، إذا تم استخدامها.
  • ما الذي نحتاجه من وضع Bash الصارم غير الرسمي ، مع فحص ميزة set -u . لا نحتاج إلى كل وضع Bash الصارم ، لأن توافق shellcheck / shellharden يعني اقتباس كل شيء وكل شيء أكثر صرامة. بالإضافة إلى ذلك ، لا ينبغي استخدام خيار set -u في Bash 4.3 والإصدارات الأقدم. نظرًا لأن هذا الخيار يعتبر المصفوفات الفارغة على أنها مهملة في تلك الإصدارات ، فلا يمكن استخدام المصفوفات للأغراض الموضحة هنا. يعد استخدام المصفوفات هو ثاني أهم نصيحة من هذا الدليل (بعد علامات الاقتباس) والسبب الوحيد الذي نضحي بالتوافق مع POSIX ، لذلك هذا غير مقبول على الإطلاق: إما عدم استخدام set -u ، أو استخدام Bash 4.4 أو أي قذيفة عادية أخرى مثل Zsh. قول هذا أسهل من فعله ، لأن هناك احتمال أن يستمر شخص ما في تشغيل النص الخاص بك في الإصدار القديم من Bash. لحسن الحظ ، كل شيء يعمل مع set -u سيعمل بدونه ( set -e لا يمكنك قول ذلك). هذا هو السبب في أنه من المهم استخدام فحص الإصدار. احذر من افتراض أن الاختبار والتطوير يتم في غلاف متوافق مع Bash 4.4 (لذلك يتم اختبار جانب set -u ). إذا كان هذا يزعجك ، فهناك خيار آخر هو رفض التوافق (يفشل البرنامج النصي عند فشل التحقق من الإصدار) ، أو يرفض set -u .
  • shopt -s nullglob for f in *.txt بشكل صحيح إذا لم يعثر *.txt على الملفات. يمرر السلوك الافتراضي (المعروف أيضًا باسم passglob ) القالب دون تغيير ، والذي يكون في حالة النتيجة الصفرية أمرًا خطيرًا لعدة أسباب. بالنسبة إلى globstar ، يؤدي ذلك إلى تنشيط البحث العودي . الاستبدال أسهل من find . لذا استخدمه.

لكن ليس:

 IFS='' set -f shopt -s failglob 

  • تعيين محدد الحقل الداخلي على سلسلة فارغة يجعل من المستحيل تقسيم الكلمة. يبدو وكأنه الحل المثالي. لسوء الحظ ، هذا بديل غير كامل للاقتباس من المتغيرات واستبدال الأوامر ، وبما أنك ستستخدم علامات الاقتباس ، فإنه لا يعطي أي شيء. السبب وراء استمرار استخدام علامات الاقتباس هو أن السلاسل الفارغة تصبح صفائف فارغة (كما في test $x = "" ) ولا يزال توسيع النموذج غير المباشر ممكنًا. علاوة على ذلك ، فإن مشاكل هذا المتغير ستسبب أيضًا مشاكل مع أوامر مثل read ، والتي تكسر الإنشاءات مثل cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done' cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done' cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done' .
  • تم تعطيل امتداد القالب: ليس فقط الامتداد غير المباشر سيئ السمعة ، ولكن أيضًا الامتداد المباشر الخالي من المتاعب ، والذي ، كما قلت ، يجب عليك استخدامه. لذا من الصعب القبول. وهذا أيضًا اختياري تمامًا للنص البرمجي المتوافق مع shellcheck / shellharden.
  • على عكس nullglob ، يفشل الفشل مع نتيجة فارغة. على الرغم من أن هذا الأمر منطقي لمعظم الأوامر ، على سبيل المثال ، rm -- *.txt (لأنه في معظم الأوامر لا يزال من غير المتوقع تنفيذه بنتيجة صفرية) ، من الواضح أنه لا يمكن استخدام الفشل إلا إذا كنت لا تتوقع نتيجة صفرية. هذا يعني أنك عادة لن تضع قوالب المجموعة في وسيطات الأمر ما لم تفترض نفس الشيء. ولكن ما يمكن أن يحدث دائمًا هو استخدام nullglob وتوسيع القالب إلى وسيطات فارغة في البنيات التي يمكن أن تأخذها ، مثل حلقة أو تعيين قيم لصفيف ( txt_files=(*.txt) ).

كيف تكمل سكربت باش


حالة خروج البرنامج النصي هي حالة آخر أمر تم تنفيذه. تأكد من أنه يمثل نجاحًا أو فشلًا حقيقيًا.

أسوأ شيء هو ترك الحل لشرط غير ذي صلة في شكل قائمة AND في نهاية البرنامج النصي. إذا كان الشرط خطأ ، فسيكون آخر أمر تم تنفيذه هو الشرط نفسه.

بالنسبة إلى errexit ، لا يتم استخدام الشروط في شكل قائمة AND مطلقًا في المقام الأول. إذا لم يتم استخدام errexit ، ففكر في معالجة الأخطاء حتى في الأمر الأخير ، لذا لن يتم إخفاء حالة الخروج إذا تمت إضافة رمز إضافي إلى البرنامج النصي.

سيء:

 condition && extra_stuff 

جيد (خيار errexit):

 if condition; then extra_stuff fi 

جيد (خيار معالجة الخطأ):

 if condition; then extra_stuff || exit fi exit 0 

كيفية استخدام errexit


مثل set -e .

تأخر مستوى البرنامج


إذا كان errexit يعمل كما ينبغي ، فاستخدمه لتثبيت أي تنظيف ضروري عند الخروج.

 tmpfile="$(mktemp -t myprogram-XXXXXX)" cleanup() { rm -f "$tmpfile" } trap cleanup EXIT 

اشتعلت: يتم تجاهل errexit في الحجج الأمر


إليكم "قنبلة" متشعبة للغاية ، وكان فهمها يستحق الكثير بالنسبة لي. عمل البرنامج النصي للبناء الخاص بي بشكل جيد على أجهزة تطوير مختلفة ، ولكن وضع خادم الإنشاء على ركبتيه:

 set -e # Fail if nproc is not installed make -j"$(nproc)" 

صحيح (استبدال الأمر في المهمة):

 set -e # Fail if nproc is not installed jobs="$(nproc)" make -j"$jobs" 

تحذير: تظل الأوامر المضمنة local export أوامر ، لذلك لا يزال هذا خطأ:

 set -e # Fail if nproc is not installed local jobs="$(nproc)" make -j"$jobs" 

يحذر ShellCheck فقط من الأوامر الخاصة مثل local في هذه الحالة.

لاستخدام local ، افصل الإعلان عن الوظيفة:

 set -e # Fail if nproc is not installed local jobs jobs="$(nproc)" make -j"$jobs" 

اشتعلت: يتم تجاهل errexit اعتمادًا على سياق المتصل


في بعض الأحيان يكون POSIX رهيبًا. يتم تجاهل Errexit في الوظائف ، وأوامر المجموعة ، وحتى القذائف الفرعية إذا تحقق المتصل من نجاحه. كل هذه الأمثلة تطبع Great success لا يمكن Unreachable Great success ، مهما بدا الأمر غريبًا.

القشرة الفرعية:

 ( set -e false echo Unreachable ) && echo Great success 

فريق المجموعة:

 { set -e false echo Unreachable } && echo Great success 

الوظيفة:

 f() { set -e false echo Unreachable } f && echo Great success 

وبسبب هذا ، فإن bash with errexit غير مناسب عمليًا للربط: نعم ، من الممكن التفاف وظائف errexit حتى تعمل ، ولكن هناك شكوك في أن الجهود المحفوظة (في معالجة الأخطاء الصريحة) تستحق ذلك. بدلًا من ذلك ، فكر في التقسيم إلى نصوص مستقلة تمامًا.

تجنب استدعاء الصدفة بعلامات اقتباس غير صحيحة


عند استدعاء أوامر من لغات برمجة أخرى ، من الأسهل ارتكاب خطأ واستدعاء الغلاف بشكل ضمني. إذا كان أمر shell هذا ثابتًا ، فهذا جيد - إما أنه يعمل أم لا. ولكن إذا كان برنامجك يعالج الخطوط بطريقة أو بأخرى لبناء هذا الأمر ، فأنت بحاجة إلى الفهم - أنت تقوم بإنشاء برنامج نصي shell ! نادرًا ما أرغب في القيام بذلك ، ومن الصعب جدًا ترتيب كل شيء بشكل صحيح:

  • اقتبس كل حجة ؛
  • الهروب من الأحرف المقابلة في الحجج.

بغض النظر عن لغة البرمجة التي تقوم بها بذلك ، هناك ثلاث طرق على الأقل لبناء فريق بشكل صحيح. بترتيب التفضيل:

خطة أ: الاستغناء عن قذيفة


إذا كان هذا مجرد أمر يحتوي على وسيطات (أي أنه لا توجد وظائف shell مثل الأنابيب أو إعادة التوجيه) ، فحدد خيار الصفيف.

  • سيء (python3): subprocess.check_call('rm -rf ' + path)
  • Good (python3): subprocess.check_call(['rm', '-rf', path])

سيء (C ++):

 std::string cmd = "rm -rf "; cmd += path; system(cmd); 

جيد (C / POSIX) ، معالجة الخطأ ناقصًا:

 char* const args[] = {"rm", "-rf", path, NULL}; pid_t child; posix_spawnp(&child, args[0], NULL, NULL, args, NULL); int status; waitpid(child, &status, 0); 

الخطة ب: نص برمجي ثابت


إذا كانت الصدفة مطلوبة ، دع الوسيطات تكون حججًا. قد تعتقد أنه كان مرهقًا كتابة نص برمجي خاص في ملفك والوصول إليه حتى ترى مثل هذه الخدعة:

سيء (python3): subprocess.check_call('docker exec {} bash -ec "printf %s {} > {}"'.format(instance, content, path))
جيد (python3): subprocess.check_call(['docker', 'exec', instance, 'bash', '-ec', 'printf %s "$0" > "$1"', content, path])

هل يمكنك ملاحظة نص الصدفة؟

هذا صحيح ، يتم إعادة توجيه الأمر printf. انتبه إلى الحجج المرقمة المقتبسة بشكل صحيح. لا بأس بتنفيذ سكربت الصدفة الثابت.

تعمل هذه الأمثلة في Docker لأنه بخلاف ذلك لن تكون مفيدة للغاية ، ولكن Docker هو أيضًا مثال رائع على أمر يقوم بتشغيل أوامر أخرى بناءً على الوسيطات. على عكس Ssh ، كما سنرى لاحقًا.

الخيار الأخير: معالجة الخط


إذا كان يجب أن يكون سلسلة (على سبيل المثال ، لأنه يجب أن يعمل من خلال ssh ) ، فلا يمكن تجاوزه. سيكون عليك اقتباس كل حجة والهروب من أي أحرف مطلوبة للخروج من هذه الاقتباسات. أسهل طريقة هي التبديل إلى علامات الاقتباس المفردة ، لأنها تحتوي على أبسط قواعد الهروب. قاعدة واحدة فقط: ''\" .

اسم الملف النموذجي المفرد:

 echo 'Don'\''t stop (12" dub mix).mp3' 

كيفية استخدام هذه الحيلة لتنفيذ أوامر ssh بأمان؟ هذا مستحيل! حسنًا ، إليك الحل "الصحيح غالبًا":

  • الحل "الصحيح غالبًا" (python3): subprocess.check_call(['ssh', 'user@host', "sha1sum '{}'".format(path.replace("'", "'\\''"))])

يجب علينا نحن أنفسنا أن نجمع كل الحجج في سلسلة حتى لا تخطئ Ssh في ذلك: إذا حاولت تمرير عدة وسيطات ssh ، فسيبدأ في دمج الحجج بشكل خادع بدون علامات اقتباس.

عادة ما يكون هذا غير ممكن لأن القرار الصحيح يعتمد على تفضيلات المستخدم على الطرف الآخر ، أي الغلاف البعيد ، والذي يمكن أن يكون أي شيء. في الأساس ، يمكن أن تكون حتى والدتك. من "الصحيح غالبًا" افتراض أن القشرة البعيدة هي bash أو قشرة أخرى متوافقة مع POSIX ، ولكن الأسماك غير متوافقة في هذه المرحلة .

Source: https://habr.com/ru/post/ar413117/


All Articles