开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
bash 使用的安全方式 - 技术翻译 - 开源中国社区

bash 使用的安全方式 【已翻译100%】

标签: <无>
李三石 推荐于 3个月前 (共 12 段, 翻译完成于 05-25) 评论 0
收藏  
0
推荐标签: 待读

为什么使用Bash?

Bash有多个数组和安全模式,在正确运用的情况下,它会让安全编码实践可以被人接受。
Fish更容易正确运用,但是缺少一个安全模式。因此在fish中做原型是一个好主意,前提是你要知道如何从fish正确地翻译到bash。

前言

这个指南使用的是ShellHarden,但是作者也推荐了ShellCheck:ShellHarden的规则可能会与ShellCheck有所不同。
Bash不是一个语言,正确的做事方法也是最容易的。如果对安全bash编码来说也有像驾照一类的东西,那它肯定是BashPitfalls的第0条:每次都使用引号。

琪花亿草
 翻译得不错哦!

首先需要了解bash编码

疯狂引用!  一个无引号的变量将被视为武装炸弹:它在与空格接触时爆炸。是的,就像“爆炸”在将字符串切分为数组的情况类似。具体来说,变量扩展(如$ var)和命令替换(如$(cmd))会进行单词分割,在此会将包含的字符串通过特殊的$IFS变量(默认值为空格)扩展为数组。这大部分是不可见的,因为大多数情况下,结果是一个1元数组,与你期望的字符串难以区分。

不仅如此,通配符(*?)也被扩展了。这个过程发生在单词分割之后,这样当结果字包含任何通配符时,该单词现在是通配符模式,它将扩展到你可能碰巧存在的任何所匹配到的文件路径。 所以这个功能实际上取决于你的文件系统!

Tocy
 翻译得不错哦!

引号可以阻止单词分割和通配符扩展, 比如在一些变量和命令置换中.

变量扩展:

  • Good: "$my_var"

  • Bad: $my_var

命令置换:

  • Good: "$(cmd)"

  • Bad: $(cmd)

当然也有一些例外情况, 这时引号不是必须的. 但是带着引号也是人畜无害的, 况且通用规则就是当你看到未被引号包围的变量时就要小心一点, 所以对于读者来说, 过于追求这些不是太显而易见的例外情况是有问题的. 一些看起来错误的实践也足以引起我们的担心: 不能正确处理文件名中空格的大量脚本正在被写出来, 而这些是需要被避免的.

一些仅有的可以被接受的例外情况是代表数值内容的变量, 比如: $?, $# 和 ${#array[@]} 等.

xiaoaiwhc1
 翻译得不错哦!

我应该使用倒引号(backticks)吗?

命令的可替代格式如下:

  • 正确: "`cmd`"

  • 错误: `cmd`

虽然可以正确的使用这种格式,它在引号中看起来更笨拙,而且当嵌套的时候会更加难读。围绕这个问题的意见相当明确:避免使用。
Shellharden 将它们重写到dollar-括号形式。

我应该使用大括号吗?

大括号用于字符串插入语,也就是,一般来说没有必要。

  • 不好的: some_command $arg1 $arg2 $arg3

  • 不好且冗长的: some_command ${arg1} ${arg2} ${arg3}

  • 好的但是冗长的: some_command "${arg1}" "${arg2}" "${arg3}"

  • 好的: some_command "$arg1" "$arg2" "$arg3"

理论上来说,总是使用大括号没什么问题,但是以作者经验来讲,在不必要的使用大括号和正确使用引号之间有较强的负相关关系——几乎每个人不选择“好的却冗长”,而使用“错误且冗长”的格式。
作者的理论:

  • 对错误情况的恐惧:一个初学者可能会担心名为$prefix的变量会影响"$prefix_postfix”的展开形式—— 这根本不是它的工作方式,而无视掉真正的危险(缺少引号)。

  • 对新兴格式的崇拜(Cargo cult)——依照约定写代码,导致这对错误情况的恐惧一直存在。

  • 在可以忍受的冗长的限制下,对使用大括号或引号的纠结。

为了禁止使用不必要的大括号,已经做出了决定:Shellharden会以最简单的好的形式重写这些变量。
现在来看基于字符串插值,即大括号真实的用途:

  • 不好的 (连接): $var1"more string content"$var2

  • 好的(连接): "$var1""more string content""$var2"

  • 好的(插入): "${var1}more string content${var2}"

在bash中连接和插入是对等的(甚至是对于数组来说,这有点荒谬)。
因为Shellharden不是一个格式化工具,普遍认为它不会改变正确的代码。这对于“好的(连接)”的例子是没错的:就Shellharden而言,这是一个神圣的(规则上正确)格式。
在需要的基础上,Shellharden会及时添加或者删除大括号:在不好的例子中,var1会使用大括号变成插入语,但由于在字符串的末尾,是从不需要大括号的,即使在好的(插入)的例子中,var2的大括号也不会被识别。后者的需求可能会被无视。

琪花亿草
 翻译得不错哦!

数字参数

不像平常的标识符变量名一样(比如正则表达式: [_a-zA-Z][_a-zA-Z0-9]*), 数字参数需要用大括号括起来(无论是否有其它字符串内插). ShellCheck 会警告:

echo "$10"
      ^-- SC1037: Braces are required for positionals over 9, e.g. ${10}. (大于9的位置参数需要大括号)

而Shellharden 会拒绝这种操作(因为这个变量定义会引起歧义).

由于大于9的数字参数需要用大括号,因此Shellharden 允许所有的数字参数都可以使用大括号。


xiaoaiwhc1
 翻译得不错哦!

使用数组

为了能够引用所有变量,必须使用真正的数组,而不是用空格分隔的伪数组字符串。

语法虽然冗长,但是必须克服它。 这种bashism仅仅是为了减少大多数shellcript的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


这就是为什么数组相当于只是一个shell的基本功能的原因:命令参数基本上是数组(而shell脚本都是关于命令和参数的)。 你可以说,一个无法通过人为的干净地传递多个参数的shell是不合适的。 这个类别中的一些广泛的shell包括Dash和Busybox Ash。 这些是最小的POSIX兼容shell - 当最重要的东西不在POSIX兼容范围中时又有什么好处呢?

凉凉_
 翻译得不错哦!

那些实际上你打算分割字符串的例外情况

以 \v 作为分隔符的示例(请注意第二次出现):

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

这避免了通配符扩展,无论分隔符是不是 \n,它都能正常工作。 如果分隔符为空,则保留最后一个元素。 出于某种原因,-d选项必须首先出现,因此将选项放在一起作为-rad'',这是诱导人的,它不起作用。 用bash 4.2,4.3和4.4测试。

或者,对于bash 4.4使用:

readarray -td $'\v' a < <(printf '%s\v' "$s")
凉凉_
 翻译得不错哦!

如何开始编写一个bash脚本

像这样:

#!/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

这里包括了:

  • hashbang:

  • 可移植性考虑:env的绝对路径可能比bash的绝对路径更具可移植性。例如:NixOS。 POSIX强制要求env的存在,但bash不是一个posix的东西。

  • 安全性考虑:这里没有像-eu pipefail这样的语言选项!使用env重定向实际上并不可行,但即使你的hashbang以#!/bin/bash开头,它也不会影响脚本含义中选项的正确位置,因为它可以被覆盖,这可能会以错误的方式运行你的脚本。但是,不会影响脚本含义的选项如set -x会成为可覆盖(如果使用它)的可选值。

  • 我们需要使用Bash的非官方严格模式,并在功能检查后面设置-u。我们并不需要所有Bash的严格模式,因为符合shellcheck/shellharden意味着引用所有内容,这是一种超越严格模式的级别。此外,在Bash 4.3及更早的版本中不能使用set -u。因为这些选项在这些版本中将空数组视为未设置,这使得数组无法用于此处。数组是本指南中第二个最重要的建议(在引用之后),也是牺牲POSIX兼容性的唯一原因,这当然是不可接受的:如果完全使用set -u,则使用Bash 4.4或Zsh等其他更明智的shell。如果有人可能会用过时版本的Bash运行脚本,那么说起来容易做起来难。幸运的是,使用set -u的工具也可以在没有数组(不像set -e)的情况下工作。因此,为什么把它放在功能检查之后是完全明智的。注意测试和开发是在Bash 4.4兼容shell的前提下(因此脚本的set -u方面经过测试)。如果这涉及到你,你的其他选择是放弃兼容性(如果功能检查失败,则失败)或放弃设置set -u。

  • shopt -s nullglob是当* .txt匹配零文件时使for f in *.txt正确工作的原因。默认状态(aka. passglob) - 如果它恰好没有匹配,按原样传递模式 - 这些原因是很危险的。至于globstar,可以实现递归通配。 Globbing比find更容易正确使用。所以请使用它。

但是不是这样使用:

IFS=''
set -f
shopt -s failglob
  • internal field separator设置为空字符串将禁用分词。听起来像做梦。可悲的是,这并不是引用变量和命令替换的完全替代品,并且假设您将使用引号,这不会给您带来任何结果。您仍然必须使用引号,否则,空字符串将变为空数组(如在test $x =“”中),并且间接通配符扩展仍处于活动状态。此外,与这个变量混淆也会像使用它的read命令混淆,重新编写如cat/etc/fstab | while read -r dev mnt fs opt dump pass: do echo "$fs": dome'。

  • 禁用通配符扩展:不仅是臭名昭着的间接扩展,而且也是无问题的直接扩展,我认为你应该使用它。所以这是一个很棘手东西。对于shellcheck/shellharden符合的脚本来说,这也是完全不必要的。

  • 作为nullglob的替代方法,如果零匹配,failglob将失败。虽然这对大多数命令有意义,例如rm - * .txt(因为大多数带有文件参数的命令不会被调用,而且无论如何都不会调用它们),显然,failglob只能在您能够使用时使用假定零匹配不会发生。这只意味着你大多不会在命令参数中使用通配符,除非你假设相同。但可以做的是,使用nullglob并让模式扩展为可以接受零参数的构造例如for循环或数组赋值(txt_files =(*.txt))中的零参数。

凉凉_
 翻译得不错哦!

如何使用errexit

又叫set -e。

程序层面的延期清理

在errexit执行它的情况下,使用它来设置在退出时发生的任何必要的清理。

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

抓住要点:Errexit在命令参数中被忽略

这是一个不错的拙劣的fork炸弹,我艰难的学会了如下方式 - 我的构建脚本在各种开发人员机器上运行良好,但是使我公司的构建服务器瘫痪了:

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在函数中被忽略,甚至作用域或子shell层中被忽略。 尽管很理智,但这些例子都会被打印出“Unreachable”和“success”。

子shell:

(
    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

这使得带有errexit的bash实际上是不能使用的 - 有可能可以包装你的errexit函数以使它们仍然可以工作,但是它节省的工作(通过显式的错误处理)会变得有问题。 考虑分割成完全独立的脚本。

凉凉_
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(0)
Ctrl/CMD+Enter

暂无网友评论
顶部