Ý nghĩa bảo mật của việc quên trích dẫn một biến trong shell bash / POSIX


206

Nếu bạn đã theo dõi unix.stackexchange.com một thời gian, bạn nên biết rằng bây giờ để lại một biến không được trích dẫn trong ngữ cảnh danh sách (như trong echo $var) trong các vỏ Bourne / POSIX (zsh là ngoại lệ) có ý nghĩa rất đặc biệt và không nên được thực hiện trừ khi bạn có một lý do rất tốt để.

Nó được thảo luận tại chiều dài trong một số Q & A ở đây (ví dụ: ? Tại sao vỏ kịch bản sặc của tôi trên khoảng trắng hoặc ký tự đặc biệt khác , Khi là hai trích dẫn cần thiết? , Mở rộng của một biến vỏ và ảnh hưởng của glob và chia vào nó , trích dẫn mở rộng chuỗi không trích dẫn)

Đó là trường hợp kể từ lần phát hành đầu tiên của vỏ Bourne vào cuối những năm 70 và không bị thay đổi bởi vỏ Korn (một trong những điều hối tiếc lớn nhất của David Korn (câu hỏi số 7) ) hoặc bashchủ yếu là sao chép vỏ Korn, và đó là Làm thế nào mà đã được chỉ định bởi POSIX / Unix.

Bây giờ, chúng tôi vẫn thấy một số câu trả lời ở đây và thậm chí đôi khi được phát hành công khai mã shell nơi các biến không được trích dẫn. Bạn đã nghĩ rằng mọi người sẽ học được bây giờ.

Theo kinh nghiệm của tôi, chủ yếu có 3 loại người bỏ qua để trích dẫn các biến của họ:

  • người mới bắt đầu. Chúng có thể được coi là một cú pháp hoàn toàn không trực quan. Và đó là vai trò của chúng tôi trên trang web này để giáo dục họ.

  • Người hay quên.

  • những người không bị thuyết phục ngay cả sau khi lặp đi lặp lại, họ nghĩ rằng chắc chắn tác giả vỏ Bourne không có ý định chúng tôi trích dẫn tất cả các biến của chúng tôi .

Có lẽ chúng ta có thể thuyết phục họ nếu chúng ta phơi bày rủi ro liên quan đến loại hành vi này.

Điều tồi tệ nhất có thể xảy ra nếu bạn quên trích dẫn các biến của mình. Nó thực sự xấu?

Chúng ta đang nói đến loại lỗ hổng nào?

Trong bối cảnh nào nó có thể là một vấn đề?


8
BashPit thác là một cái gì đó bạn sẽ như tôi nghĩ.
pawel7318

backlink từ bài viết này tôi đã viết , cảm ơn vì đã viết
mirabilos

5
Tôi muốn đề nghị thêm một nhóm thứ tư: những người đã bị đánh vào đầu cho quá mức trích dẫn quá nhiều lần, có lẽ bởi các thành viên của nhóm thứ ba đưa ra khỏi sự thất vọng của họ vào người khác (nạn nhân trở thành kẻ bắt nạt). Điều đáng buồn tất nhiên là những người thuộc nhóm thứ tư cuối cùng có thể không trích dẫn những điều khi nó quan trọng nhất.
ack

Câu trả lời:


201

Lời nói đầu

Đầu tiên, tôi muốn nói rằng đó không phải là cách đúng đắn để giải quyết vấn đề. Đó là một chút giống như nói rằng " bạn không nên giết người vì nếu không bạn sẽ đi tù ".

Tương tự, bạn không trích dẫn biến của mình vì nếu không, bạn sẽ đưa ra các lỗ hổng bảo mật. Bạn trích dẫn các biến của bạn bởi vì nó không sai (nhưng nếu nỗi sợ của nhà tù có thể giúp đỡ, tại sao không).

Một tóm tắt nhỏ cho những người vừa nhảy lên tàu.

Trong hầu hết các shell, việc mở rộng biến không được trích dẫn (mặc dù điều đó (và phần còn lại của câu trả lời này) cũng áp dụng cho thay thế lệnh ( `...`hoặc $(...)) và mở rộng số học ( $((...))hoặc $[...])) có ý nghĩa rất đặc biệt. Cách tốt nhất để mô tả nó là nó giống như gọi một số loại toán tử ẩn + toán tử toàn cầu¹ .

cmd $var

trong một ngôn ngữ khác sẽ được viết một cái gì đó như:

cmd(glob(split($var)))

$varđầu tiên được chia thành một danh sách các từ theo các quy tắc phức tạp liên quan đến $IFStham số đặc biệt (phần tách ) và sau đó mỗi từ tạo ra sự phân tách đó được coi là một mẫu được mở rộng thành một danh sách các tệp khớp với nó (phần toàn cầu ) .

Ví dụ, nếu $varchứa *.txt,/var/*.xml$IFS chứa ,, cmdsẽ được gọi với một số đối số, đối số đầu tiên cmdvà đối số tiếp theo là các txt tệp trong thư mục hiện tại và các xmltệp trong /var.

Nếu bạn muốn gọi cmdchỉ bằng hai đối số theo nghĩa đen cmd*.txt,/var/*.xml, bạn sẽ viết:

cmd "$var"

đó sẽ là ngôn ngữ quen thuộc khác của bạn:

cmd($var)

Chúng ta có ý nghĩa gì bởi lỗ hổng trong vỏ ?

Rốt cuộc, người ta đã biết từ thời bình minh rằng các kịch bản shell không nên được sử dụng trong các bối cảnh nhạy cảm về bảo mật. Chắc chắn, OK, để lại một biến không được trích dẫn là một lỗi nhưng điều đó không thể làm hại nhiều như vậy, phải không?

Chà, mặc dù thực tế là bất cứ ai cũng sẽ nói với bạn rằng các kịch bản shell không bao giờ được sử dụng cho CGI web, hoặc may mắn là hầu hết các hệ thống không cho phép các tập lệnh shell setuid / setgid hiện nay, một điều mà shellshock (lỗi bash có thể khai thác từ xa đã tạo ra tiêu đề vào tháng 9 năm 2014) tiết lộ rằng các shell vẫn được sử dụng rộng rãi ở những nơi mà chúng có lẽ không nên: trong CGI, trong các kịch bản hook của máy khách DHCP, trong các lệnh sudoers, được gọi bằng (nếu không ) các lệnh setuid ...

Đôi khi vô tình. Ví dụ, system('cmd $PATH_INFO') trong tập lệnh php/ perl/ pythonCGI không gọi shell để diễn giải dòng lệnh đó (không đề cập đến thực tế rằng cmdchính nó có thể là tập lệnh shell và tác giả của nó có thể chưa bao giờ mong đợi nó được gọi từ CGI).

Bạn đã có một lỗ hổng khi có một con đường leo thang đặc quyền, đó là khi ai đó (hãy gọi anh ta là kẻ tấn công ) có thể làm điều gì đó mà anh ta không có ý định.

Luôn luôn có nghĩa là kẻ tấn công cung cấp dữ liệu, dữ liệu đó được xử lý bởi người dùng / quy trình đặc quyền vô tình làm điều gì đó không nên làm, trong hầu hết các trường hợp vì lỗi.

Về cơ bản, bạn đã gặp sự cố khi mã lỗi của bạn xử lý dữ liệu dưới sự kiểm soát của kẻ tấn công .

Bây giờ, không phải lúc nào dữ liệu đó cũng có thể đến từ đâu và thường rất khó để biết liệu mã của bạn có được xử lý dữ liệu không đáng tin hay không.

Đối với các biến có liên quan, trong trường hợp tập lệnh CGI, khá rõ ràng, dữ liệu là các tham số CGI GET / POST và những thứ như cookie, đường dẫn, máy chủ ... tham số.

Đối với tập lệnh setuid (chạy như một người dùng khi được người khác gọi), đó là đối số hoặc biến môi trường.

Một vector rất phổ biến là tên tệp. Nếu bạn nhận được một danh sách tệp từ một thư mục, có thể các tệp đó đã được kẻ tấn công trồng ở đó .

Về vấn đề đó, ngay cả tại dấu nhắc của trình bao tương tác, bạn có thể dễ bị tổn thương (khi xử lý tệp trong /tmphoặc ~/tmp ví dụ).

Thậm chí một ~/.bashrccó thể dễ bị tổn thương (ví dụ, bashsẽ diễn giải nó khi được gọi sshđể chạy ForcedCommand like trong gittriển khai máy chủ với một số biến dưới sự kiểm soát của máy khách).

Bây giờ, một tập lệnh có thể không được gọi trực tiếp để xử lý dữ liệu không đáng tin cậy, nhưng nó có thể được gọi bởi một lệnh khác. Hoặc mã không chính xác của bạn có thể được sao chép-dán vào các tập lệnh thực hiện (bởi bạn 3 năm xuống dòng hoặc một trong những đồng nghiệp của bạn). Một nơi đặc biệt quan trọng là trong các câu trả lời trong các trang web Hỏi & Đáp vì bạn sẽ không bao giờ biết bản sao mã của mình có thể ở đâu.

Xuống kinh doanh; nó tệ như thế nào

Để lại một biến (hoặc thay thế lệnh) không được trích dẫn là nguồn lỗ hổng bảo mật số một liên quan đến mã shell. Một phần vì những lỗi đó thường chuyển thành các lỗ hổng nhưng cũng vì nó rất phổ biến để xem các biến không được trích dẫn.

Trên thực tế, khi tìm kiếm các lỗ hổng trong mã shell, điều đầu tiên cần làm là tìm kiếm các biến không được trích dẫn. Thật dễ dàng để nhận ra, thường là một ứng cử viên tốt, thường dễ theo dõi dữ liệu do kẻ tấn công kiểm soát.

Có vô số cách mà một biến không được trích dẫn có thể biến thành một lỗ hổng. Tôi sẽ chỉ đưa ra một vài xu hướng phổ biến ở đây.

Công bố thông tin

Hầu hết mọi người sẽ gặp phải các lỗi liên quan đến các biến không được trích dẫn do phần bị tách (ví dụ: hiện tại các tệp có khoảng trắng trong tên của họ và không gian nằm trong giá trị mặc định của IFS). Nhiều người sẽ bỏ qua phần toàn cầu . Phần toàn cầu ít nhất là nguy hiểm như phần tách .

Globbing được thực hiện khi đầu vào bên ngoài không được xác nhận có nghĩa là kẻ tấn công có thể khiến bạn đọc nội dung của bất kỳ thư mục nào.

Trong:

echo You entered: $unsanitised_external_input

nếu $unsanitised_external_inputchứa /*, điều đó có nghĩa là kẻ tấn công có thể thấy nội dung của /. Không vấn đề gì. Nó trở nên thú vị hơn mặc dù /home/*nó cung cấp cho bạn một danh sách tên người dùng trên máy /tmp/*, /home/*/.forwardđể gợi ý về các thực hành nguy hiểm khác, /etc/rc*/*cho các dịch vụ được kích hoạt ... Không cần phải đặt tên riêng cho họ. Một giá trị /* /*/* /*/*/*...sẽ chỉ liệt kê toàn bộ hệ thống tập tin.

Từ chối các lỗ hổng dịch vụ.

Lấy trường hợp trước một chút quá xa và chúng ta đã có DoS.

Trên thực tế, bất kỳ biến không được trích dẫn nào trong ngữ cảnh danh sách với đầu vào không được xác nhận ít nhất là một lỗ hổng DoS.

Ngay cả các chuyên gia viết kịch bản shell thường quên trích dẫn những điều như:

#! /bin/sh -
: ${QUERYSTRING=$1}

:là lệnh no-op. Cái gì có thể đi sai?

Điều đó có nghĩa là gán $1cho $QUERYSTRINGnếu $QUERYSTRING không được đặt. Đó cũng là một cách nhanh chóng để tạo tập lệnh CGI có thể gọi được từ dòng lệnh.

Điều đó $QUERYSTRINGvẫn được mở rộng mặc dù và vì nó không được trích dẫn, toán tử split + global được gọi.

Bây giờ, có một số quả cầu đặc biệt đắt tiền để mở rộng. Các /*/*/*/*một là đủ xấu vì nó có nghĩa là thư mục niêm yết lên đến 4 cấp độ xuống. Ngoài hoạt động của đĩa và CPU, điều đó có nghĩa là lưu trữ hàng chục nghìn đường dẫn tệp (40k ở đây trên máy chủ VM tối thiểu, 10k trong số các thư mục).

Bây giờ /*/*/*/*/../../../../*/*/*/*có nghĩa là 40k x 10k và /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*đủ để đưa cả cỗ máy mạnh nhất đến đầu gối của nó.

Hãy tự mình thử (mặc dù hãy chuẩn bị cho máy của bạn gặp sự cố hoặc treo):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Tất nhiên, nếu mã là:

echo $QUERYSTRING > /some/file

Sau đó, bạn có thể điền vào đĩa.

Chỉ cần thực hiện tìm kiếm google trên shell cgi hoặc bash cgi hoặc ksh cgi , và bạn sẽ tìm thấy một vài trang chỉ cho bạn cách viết CGI bằng shell. Lưu ý rằng một nửa trong số đó là các tham số quá trình dễ bị tổn thương.

Ngay cả cái riêng của David Korn cũng dễ bị tổn thương (nhìn vào cách xử lý cookie).

lên đến các lỗ hổng thực thi mã tùy ý

Thực thi mã tùy ý là loại lỗ hổng tồi tệ nhất, vì nếu kẻ tấn công có thể chạy bất kỳ lệnh nào, không có giới hạn về những gì anh ta có thể làm.

Đó thường là phần tách ra dẫn đến những điều đó. Việc phân tách đó dẫn đến một số đối số được truyền cho các lệnh khi chỉ có một đối số được mong đợi. Trong khi những cái đầu tiên sẽ được sử dụng trong bối cảnh dự kiến, những cái khác sẽ ở trong một bối cảnh khác nên có khả năng diễn giải khác nhau. Tốt hơn với một ví dụ:

awk -v foo=$external_input '$2 == foo'

Ở đây, ý định là gán nội dung của $external_inputbiến shell cho foo awkbiến.

Hiện nay:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

Từ thứ hai dẫn đến việc chia tách $external_input không được gán cho foonhưng được coi là awkmã (ở đây thực thi một lệnh tùy ý uname:).

Đó là đặc biệt là một vấn đề đối với các lệnh có thể thực hiện các lệnh khác ( awk, env, sed(GNU một), perl, find...) đặc biệt là với các biến thể GNU (mà chấp nhận tùy chọn sau khi tranh cãi). Đôi khi, bạn sẽ không nghi ngờ lệnh để có thể thực hiện khác như ksh, bashhoặc zsh's [hoặc printf...

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Nếu chúng ta tạo một thư mục được gọi x -o yes, thì thử nghiệm sẽ trở nên tích cực, bởi vì đó là một biểu thức điều kiện hoàn toàn khác mà chúng ta đang đánh giá.

Tồi tệ hơn, nếu chúng ta tạo một tệp được gọi x -a a[0$(uname>&2)] -gt 1, với ít nhất tất cả các triển khai ksh (bao gồm sh hầu hết các Unice thương mại và một số BSD), sẽ thực thi uname vì các shell đó thực hiện đánh giá số học trên các toán tử so sánh số của [lệnh.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

Tương tự với bashmột tên tệp như x -a -v a[0$(uname>&2)].

Tất nhiên, nếu họ không thể thực hiện tùy ý, kẻ tấn công có thể giải quyết thiệt hại ít hơn (điều này có thể giúp thực hiện tùy ý). Bất kỳ lệnh nào có thể ghi tệp hoặc thay đổi quyền, quyền sở hữu hoặc có bất kỳ tác dụng chính hoặc phụ nào đều có thể được khai thác.

Tất cả các loại điều có thể được thực hiện với tên tập tin.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

Và cuối cùng bạn ..có thể viết được (đệ quy với GNU chmod).

Các tập lệnh xử lý tự động các tập tin trong các khu vực có thể ghi công khai như /tmpđược viết rất cẩn thận.

Thế còn [ $# -gt 1 ]

Đó là điều tôi thấy bực tức. Một số người đi xuống tất cả các rắc rối của việc tự hỏi liệu một bản mở rộng cụ thể có thể có vấn đề để quyết định nếu họ có thể bỏ qua các trích dẫn.

Nó giống như nói. Này, có vẻ như $#không thể chịu sự điều chỉnh của toán tử split + global, hãy yêu cầu shell chia + global nó . Hoặc Hey, chúng ta hãy viết mã không chính xác chỉ vì lỗi không có khả năng bị tấn công .

Bây giờ làm thế nào là không thể? OK, $#(hoặc $!, $?hoặc bất kỳ sự thay thế số học nào) chỉ có thể chứa các chữ số (hoặc -đối với một số) vì vậy phần toàn cầu bị loại bỏ. Đối với phần tách để làm một cái gì đó mặc dù, tất cả những gì chúng ta cần là $IFSđể chứa các chữ số (hoặc -).

Với một số vỏ, $IFScó thể được thừa hưởng từ môi trường, nhưng nếu môi trường không an toàn, dù sao thì đó cũng là trò chơi.

Bây giờ nếu bạn viết một hàm như:

my_function() {
  [ $# -eq 2 ] || return
  ...
}

Điều đó có nghĩa là hành vi của chức năng của bạn phụ thuộc vào bối cảnh mà nó được gọi. Hay nói cách khác, $IFS trở thành một trong những đầu vào của nó. Nói đúng ra, khi bạn viết tài liệu API cho chức năng của mình, nó sẽ giống như:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

Và mã gọi hàm của bạn cần đảm bảo $IFSkhông chứa chữ số. Tất cả điều đó bởi vì bạn không cảm thấy muốn gõ 2 ký tự trích dẫn kép đó.

Bây giờ, để [ $# -eq 2 ]lỗi đó trở thành một lỗ hổng, bạn cần bằng cách nào đó để giá trị của $IFSnó trở thành sự kiểm soát của kẻ tấn công . Có thể hiểu được, điều đó thường sẽ không xảy ra trừ khi kẻ tấn công tìm cách khai thác một lỗi khác.

Điều đó không phải là chưa từng nghe thấy. Một trường hợp phổ biến là khi mọi người quên vệ sinh dữ liệu trước khi sử dụng nó trong biểu thức số học. Chúng ta đã thấy ở trên rằng nó có thể cho phép thực thi mã tùy ý trong một số shell, nhưng trong tất cả chúng, nó cho phép kẻ tấn công cung cấp cho bất kỳ biến nào một giá trị nguyên.

Ví dụ:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

Và với $1giá trị có (IFS=-1234567890), đánh giá số học đó có tác dụng phụ của cài đặt IFS và [ lệnh tiếp theo không thành công, điều đó có nghĩa là việc kiểm tra quá nhiều đối số bị bỏ qua.

Thế còn khi toán tử split + global không được gọi?

Có một trường hợp khác trong đó các trích dẫn là cần thiết xung quanh các biến và các mở rộng khác: khi nó được sử dụng làm mẫu.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

không kiểm tra xem $a$bđều giống nhau (trừ trường hợp zsh) nhưng nếu $aphù hợp với mô hình trong $b. Và bạn cần phải trích dẫn $bnếu bạn muốn so sánh như dây đàn (điều tương tự trong "${a#$b}"hoặc "${a%$b}"hoặc "${a##*$b*}"nơi $bnên được trích dẫn nếu nó không được thực hiện như một mô hình).

Điều đó có nghĩa là [[ $a = $b ]]có thể trở thành sự thật trong trường hợp $alà khác nhau từ $b(ví dụ khi $aanything$b*) hoặc có thể trả về false khi họ là giống hệt nhau (ví dụ khi cả hai $a$b[a]).

Điều đó có thể làm cho một lỗ hổng bảo mật? Vâng, giống như bất kỳ lỗi nào. Tại đây, kẻ tấn công có thể thay đổi luồng mã logic của tập lệnh của bạn và / hoặc phá vỡ các giả định mà tập lệnh của bạn đang thực hiện. Chẳng hạn, với một mã như:

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

Kẻ tấn công có thể bỏ qua kiểm tra bằng cách vượt qua '[a]' '[a]'.

Bây giờ, nếu cả mô hình đó không khớp với toán tử split + global , thì nguy cơ của việc biến một biến không được trích dẫn là gì?

Tôi phải thừa nhận rằng tôi viết:

a=$b
case $a in...

Ở đó, trích dẫn không gây hại nhưng không thực sự cần thiết.

Tuy nhiên, một tác dụng phụ của việc bỏ dấu ngoặc kép trong các trường hợp đó (ví dụ như trong câu trả lời Q & A) là nó có thể gửi một thông điệp sai cho người mới bắt đầu: rằng có thể không được trích dẫn các biến .

Ví dụ, họ có thể bắt đầu nghĩ rằng nếu a=$bổn, thì export a=$bcũng sẽ như vậy (điều này không có trong nhiều shell như trong các đối số của exportlệnh trong ngữ cảnh danh sách) hoặc env a=$b.

Thế còn zsh?

zshđã sửa chữa hầu hết những vụng về thiết kế. Trong zsh(ít nhất là khi không sh / ksh chế độ thi đua), nếu bạn muốn tách , hoặc globbing , hoặc phù hợp với mô hình , bạn phải yêu cầu nó một cách rõ ràng: $=varđể phân chia, và $~varđể glob hoặc về nội dung của các biến được đối xử như một mô hình.

Tuy nhiên, việc chia tách (nhưng không phải toàn cầu hóa) vẫn được thực hiện hoàn toàn khi thay thế lệnh không được trích dẫn (như trong echo $(cmd)).

Ngoài ra, một tác dụng phụ đôi khi không mong muốn của việc không trích dẫn biến là loại bỏ trống . Các zshhành vi tương tự như những gì bạn có thể đạt được trong vỏ khác bằng cách tắt globbing hoàn toàn (với set -f) và tách (với IFS=''). Tuy nhiên, trong:

cmd $var

Sẽ không có split + global , nhưng nếu $vartrống, thay vì nhận một đối số trống, cmdsẽ không nhận được đối số nào cả.

Điều đó có thể gây ra lỗi (như hiển nhiên [ -n $var ]). Điều đó có thể có thể phá vỡ các kỳ vọng và giả định của một kịch bản và gây ra các lỗ hổng, nhưng tôi không thể đưa ra một ví dụ không quá xa vời ngay bây giờ).

Còn khi bạn làm cần chia + glob điều hành?

Có, đó thường là khi bạn không muốn bỏ qua biến của mình. Nhưng sau đó, bạn cần đảm bảo rằng bạn điều chỉnh chính xác các toán tử phân tách và toàn cục trước khi sử dụng nó. Nếu bạn chỉ muốn phần tách chứ không phải phần toàn cầu (đó là trường hợp thường xuyên nhất), thì bạn cần phải vô hiệu hóa Globing ( set -o noglob/ set -f) và sửa lỗi $IFS. Nếu không, bạn cũng sẽ gây ra lỗ hổng (như ví dụ CGI của David Korn đã đề cập ở trên).

Phần kết luận

Nói tóm lại, việc để một biến (hoặc thay thế lệnh hoặc mở rộng số học) không được trích dẫn trong shell có thể rất nguy hiểm, đặc biệt là khi thực hiện trong bối cảnh sai và rất khó để biết đó là những bối cảnh sai.

Đó là một trong những lý do tại sao nó được coi là thực hành xấu .

Cảm ơn đã đọc cho đến nay. Nếu nó đi qua đầu bạn, đừng lo lắng. Người ta không thể mong mọi người hiểu tất cả ý nghĩa của việc viết mã theo cách họ viết. Đó là lý do tại sao chúng tôi có các khuyến nghị thực hành tốt , vì vậy chúng có thể được theo dõi mà không nhất thiết phải hiểu tại sao.

(và trong trường hợp chưa rõ ràng, vui lòng tránh viết mã nhạy cảm bảo mật bằng shell).

xin vui lòng trích dẫn các biến của bạn trên câu trả lời của bạn trên trang web này!


¹Trong ksh93pdkshcác dẫn xuất, việc mở rộng dấu ngoặc cũng được thực hiện trừ khi tính năng toàn cầu hóa bị tắt (trong trường hợp ksh93phiên bản lên đến ksh93u +, ngay cả khi braceexpandtùy chọn bị tắt).


Lưu ý rằng, với [[, chỉ có RHS so sánh cần được trích dẫn:if [[ $1 = "$2" ]]; then
mirabilos

2
@mirabilos, vâng, nhưng LHS cần không không được trích dẫn, vì vậy không có lý do gì không để trích dẫn nó ở đó (nếu chúng tôi để có những quyết định có ý thức để trích dẫn theo mặc định vì nó có vẻ là điều hợp lý nhất để làm ). Cũng lưu ý rằng điều đó [[ $* = "$var" ]]không giống như [[ "$*" = "$var" ]]nếu ký tự đầu tiên $IFSkhông phải là khoảng trắng với bash(và cũng là mkshnếu $IFStrống mặc dù trong trường hợp đó tôi không chắc điều gì $*tương đương, tôi có nên đưa ra lỗi không?)).
Stéphane Chazelas

1
Vâng, bạn có thể trích dẫn theo mặc định ở đó. Xin vui lòng không có thêm lỗi về việc chia tách trường ngay bây giờ, tôi vẫn phải sửa những lỗi tôi biết (từ bạn và những người khác) trước khi chúng tôi có thể đánh giá lại điều này.
mirabilos

2
@Barmar, giả sử bạn có nghĩa là foo='bar; rm *'không, tuy nhiên nó sẽ liệt kê nội dung của thư mục hiện tại có thể được tính là một tiết lộ thông tin. print $footrong ksh93(nơi printthay thế cho echođịa chỉ một số thiếu sót của nó) có lỗ hổng tiêm mã mặc dù (ví dụ với foo='-f%.0d z[0$(uname>&2)]') (bạn thực sự cần print -r -- "$foo". echo "$foo"vẫn sai và không thể sửa chữa (mặc dù nói chung là ít gây hại hơn)).
Stéphane Chazelas

3
Tôi không phải là chuyên gia bash, nhưng tôi đã viết mã trong hơn một thập kỷ. Tôi đã sử dụng dấu ngoặc kép rất nhiều, nhưng chủ yếu là để xử lý các khoảng trống được nhúng. Bây giờ, tôi sẽ sử dụng chúng nhiều hơn nữa! Sẽ thật tuyệt nếu ai đó mở rộng câu trả lời này để làm cho việc tiếp thu tất cả các điểm tốt trở nên dễ dàng hơn một chút. Tôi đã nhận được rất nhiều, nhưng tôi cũng bỏ lỡ rất nhiều. Đó là một bài viết dài, nhưng tôi biết có nhiều hơn ở đây để tôi tìm hiểu. Cảm ơn!
Joe

34

[Lấy cảm hứng từ câu trả lời này bởi cas .]

Nhưng nếu như thì sao?

Nhưng điều gì sẽ xảy ra nếu tập lệnh của tôi đặt một biến thành một giá trị đã biết trước khi sử dụng nó? Cụ thể, điều gì sẽ xảy ra nếu nó đặt một biến thành một trong hai hoặc nhiều giá trị có thể (nhưng nó luôn đặt nó thành một cái gì đó đã biết) và không có giá trị nào chứa không gian hoặc ký tự toàn cầu? Không an toàn để sử dụng nó mà không có dấu ngoặc kép trong trường hợp đó ?

Và điều gì sẽ xảy ra nếu một trong những giá trị có thể là chuỗi trống và tôi phụ thuộc vào việc loại bỏ trống của xóa? Tức là, nếu biến chứa chuỗi rỗng, tôi không muốn nhận chuỗi trống trong lệnh của mình; Tôi muốn nhận được gì. Ví dụ,

nếu một số điều kiện
sau đó
    ignorecase = "- i"
khác
    ignorecase = ""
fi
                                        # Lưu ý rằng các trích dẫn trong các lệnh trên không thực sự cần thiết. 
grep $   ignorecase other_ grep _args

Tôi không thể nói ; Điều đó sẽ thất bại nếu là chuỗi rỗng.grep "$ignorecase" other_grep_args$ignorecase

Phản ứng:

Như đã thảo luận trong câu trả lời khác, điều này vẫn sẽ thất bại nếu IFSchứa một -hoặc một i. Nếu bạn đã đảm bảo rằng IFSkhông chứa bất kỳ ký tự nào trong biến của bạn (và bạn chắc chắn rằng biến của bạn không chứa bất kỳ ký tự toàn cầu nào), thì điều này có thể an toàn.

Nhưng có một cách an toàn hơn (mặc dù nó hơi xấu và khá không trực quan): sử dụng ${ignorecase:+"$ignorecase"}. Từ đặc tả ngôn ngữ lệnh POSIX Shell , theo  2.6.2 Mở rộng tham số ,

${parameter:+[word]}

    Sử dụng giá trị thay thế.   Nếu parameterkhông được đặt hoặc null, null sẽ được thay thế; mặt khác, việc mở rộng word (hoặc một chuỗi rỗng nếu wordbị bỏ qua) sẽ được thay thế.

Bí quyết ở đây, chẳng hạn như nó có, là chúng ta đang sử dụng ignorecasenhư parameter"$ignorecase"như word. Có ${ignorecase:+"$ignorecase"}nghĩa là

Nếu $ignorecasekhông được đặt hoặc null (nghĩa là trống), null (nghĩa là không được trích dẫn ) sẽ được thay thế; mặt khác, việc mở rộng "$ignorecase"sẽ được thay thế.

Điều này đưa chúng ta đến nơi chúng ta muốn đến: nếu biến được đặt thành chuỗi trống, nó sẽ bị xóa bỏ ra (một biểu thức phức tạp này sẽ đánh giá là không có gì - thậm chí không phải là một chuỗi rỗng) và nếu biến đó có một chuỗi không -giá trị, chúng tôi nhận được giá trị đó, trích dẫn.


Nhưng nếu như thì sao?

Nhưng nếu tôi có một biến mà tôi muốn / cần được chia thành các từ thì sao? . ranh giới.
PS Tôi vẫn muốn xóa trống.)

Ví dụ,

nếu một số điều kiện
sau đó
    tiêu chí = "- loại f"
khác
    tiêu chí = ""
fi
nếu some_other_condition
sau đó
    iteria = "$ iteria -mtime +42"
fi
tìm "$ start_directory" $   iteria_ find _args

Phản ứng:

Bạn có thể nghĩ rằng đây là một trường hợp để sử dụng eval.  Không!   Chống lại sự cám dỗ để thậm chí nghĩ về việc sử dụng evalở đây.

Một lần nữa, nếu bạn đã đảm bảo rằng IFSkhông chứa bất kỳ ký tự nào trong biến của bạn (ngoại trừ khoảng trắng mà bạn muốn được vinh danh) và bạn chắc chắn rằng biến của bạn không chứa bất kỳ ký tự toàn cầu nào, thì có lẽ ở trên an toàn

Nhưng, nếu bạn đang sử dụng bash (hoặc ksh, zsh hoặc yash), có một cách an toàn hơn: sử dụng một mảng:

nếu một số điều kiện
sau đó
    iteria = (- loại f) # Bạn có thể nói `iteria = (" - loại "" f ")`, nhưng nó thực sự không cần thiết.
khác
    iteria = () # Không sử dụng bất kỳ dấu ngoặc kép nào trong lệnh này!
fi
nếu some_other_condition
sau đó
    iteria + = (- mtime +42) # Lưu ý: không phải `=`, mà là ` + =`, để thêm (chắp thêm) vào một mảng.
fi
tìm "$ start_directory" "$ {iteria [@]}"   other_ find _args

Từ bash (1) ,

Bất kỳ yếu tố nào của một mảng có thể được tham chiếu bằng cách sử dụng . Nếu là hoặc  , từ mở rộng cho tất cả các thành viên của . Các mục con này chỉ khác nhau khi từ xuất hiện trong dấu ngoặc kép. Nếu từ được trích dẫn kép, thì sẽ mở rộng từng thành phần của một từ riêng biệt.${name[subscript]}subscript@*name${name[@]}name

Vì vậy, "${criteria[@]}"mở rộng đến (trong ví dụ trên) các phần tử 0, hai hoặc bốn của criteriamảng, mỗi phần được trích dẫn. Cụ thể, nếu cả hai điều kiện  s đều không đúng, thì criteriamảng không có nội dung (như được đặt bởi criteria=()câu lệnh) và "${criteria[@]}"ước tính không có gì (thậm chí không phải là một chuỗi rỗng bất tiện).


Điều này đặc biệt thú vị và phức tạp khi bạn đang xử lý nhiều từ, một số từ đầu vào động (người dùng) mà bạn không biết trước và có thể chứa dấu cách hoặc ký tự đặc biệt khác. Xem xét:

printf "Nhập tên tệp cần tìm:"
đọc tên
nếu ["$ fname"! = ""]
sau đó
    tiêu chí + = (- tên "$ fname")
fi

Lưu ý rằng $fnameđược trích dẫn mỗi khi nó được sử dụng. Điều này hoạt động ngay cả khi người dùng nhập một cái gì đó như foo barhoặc foo*"${criteria[@]}"đánh giá -name "foo bar"hoặc -name "foo*". (Hãy nhớ rằng mỗi phần tử của mảng được trích dẫn.)

Mảng không hoạt động trong tất cả các vỏ POSIX; mảng là một ksh / bash / zsh / yash-ism. Ngoại trừ việc có một mảng mà tất cả các shell hỗ trợ: danh sách đối số, aka "$@". Nếu bạn đã thực hiện xong với danh sách đối số mà bạn đã được gọi (ví dụ: bạn đã sao chép tất cả các tham số vị trí của thành phố (các đối số) thành các biến hoặc xử lý chúng), bạn có thể sử dụng danh sách đối số dưới dạng một mảng:

nếu một số điều kiện
sau đó
    set - -type f # Bạn có thể nói `set -" -type "" f "`, nhưng nó thực sự không cần thiết.
khác
    bộ --
fi
nếu some_other_condition
sau đó
    đã đặt - "$ @" -mtime +42
fi
# Tương tự: đặt - "$ @" -name "$ fname"
tìm "$ start_directory" "$ @"   other_ find _args

Cấu "$@"trúc (trong lịch sử, xuất hiện đầu tiên) có cùng ngữ nghĩa với - nó mở rộng từng đối số (nghĩa là từng thành phần của danh sách đối số) thành một từ riêng biệt, như thể bạn đã gõ ."${name[@]}""$1" "$2" "$3" …

Trích từ đặc tả ngôn ngữ lệnh POSIX Shell , theo 2.5.2 Thông số đặc biệt ,

@

    Mở rộng đến các tham số vị trí, bắt đầu từ một, ban đầu tạo một trường cho mỗi tham số vị trí được đặt. Các trường ban đầu, các trường ban đầu sẽ được giữ lại làm các trường riêng biệt. Nếu không có tham số vị trí, việc mở rộng @sẽ tạo ra các trường bằng 0, ngay cả khi @nằm trong dấu ngoặc kép; Giáo dục

Toàn văn có phần khó hiểu; điểm quan trọng là nó chỉ định rằng "$@"sẽ tạo ra các trường 0 khi không có tham số vị trí. Ghi chú lịch sử: khi "$@"lần đầu tiên được giới thiệu trong shell Bourne (tiền thân của bash) vào năm 1979, nó đã có một lỗi "$@"được thay thế bằng một chuỗi rỗng duy nhất khi không có tham số vị trí; xem Điều gì có ${1+"$@"}nghĩa trong một tập lệnh shell và nó khác với "$@"như thế nào? Gia đình Bourne Shell truyền thống${1+"$@"}nghĩa là gì ... và nó cần thiết ở đâu? "$@"so với${1+"$@"} .


Mảng cũng giúp với tình huống đầu tiên:

nếu một số điều kiện
sau đó
    ignorecase = (- i) # Bạn có thể nói `ignorecase = (" - i ")`, nhưng nó thực sự không cần thiết.
khác
    ignorecase = () # Không sử dụng bất kỳ dấu ngoặc kép nào trong lệnh này!
fi
grep "$ {ignorecase [@]}"   other_ grep _args

Ngày

PS (csh)

Điều này không cần phải nói, nhưng, vì lợi ích của những người mới ở đây: csh, tcsh, v.v., không phải là vỏ Bourne / POSIX. Họ là một gia đình hoàn toàn khác nhau. Một con ngựa có màu khác. Một trò chơi bóng khác. Một giống mèo khác nhau. Chim của lông khác. Và, đặc biệt nhất, một lon giun khác.

Một số điều được nói trên trang này áp dụng cho csh; chẳng hạn như: nên trích dẫn tất cả các biến của bạn trừ khi bạn có lý do chính đáng để không và bạn chắc chắn rằng bạn biết bạn đang làm gì. Nhưng, trong csh, mỗi biến là một mảng - thực tế là hầu như mọi biến chỉ là một mảng chỉ có một phần tử và hoạt động khá giống với một biến shell thông thường trong shell Bourne / POSIX. Và cú pháp rất khác nhau (và tôi có nghĩa là hết sức ). Vì vậy, chúng tôi sẽ không nói gì thêm về vỏ gia đình csh ở đây.


1
Lưu ý rằng csh, bạn muốn sử dụng $var:qhơn "$var"là cái sau không hoạt động cho các biến có chứa các ký tự dòng mới (hoặc nếu bạn muốn trích dẫn các phần tử của mảng riêng lẻ, thay vì nối chúng với khoảng trắng thành một đối số).
Stéphane Chazelas

Trong bash, bitrate="--bitrate 320"làm việc với ${bitrate:+$bitrate}bitrate=--bitrate 128sẽ không hoạt động ${bitrate:+"$bitrate"}vì nó phá vỡ lệnh. Có an toàn để sử dụng ${variable:+$variable}với không "?
Freedo

@Freedo: Tôi thấy bình luận của bạn không rõ ràng. Tôi đặc biệt không rõ những gì bạn muốn biết mà tôi chưa nói. Xem phần thứ hai Nhưng điều gì xảy ra nếu Tiêu đề của nhóm - Từ Nhưng nếu tôi có một biến mà tôi muốn / cần được chia thành các từ thì thì đó là tình huống của bạn, và bạn nên làm theo lời khuyên ở đó. Nhưng nếu bạn không thể (ví dụ: vì bạn đang sử dụng shell không phải là bash, ksh, zsh hoặc yash và bạn đang sử dụng  $@cho mục đích khác) hoặc bạn chỉ từ chối sử dụng một mảng vì những lý do khác, tôi tham khảo bạn đến với hội nghị Như đã thảo luận trong câu trả lời khác. Tiết (Cont'd)
G-Man

(Tiếp theo) ... đề nghị của bạn (sử dụng ${variable:+$variable}không có ") sẽ thất bại nếu IFS chứa  -,  0, 2, 3, a, b, e, i, rhoặc  t.
G-Man

Vâng, biến của tôi có cả hai ký tự a, e, b, i 2 và 3 và vẫn hoạt động tốt với${bitrate:+$bitrate}
Freedo

11

Tôi đã hoài nghi về câu trả lời của Stéphane, tuy nhiên có thể lạm dụng $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

hoặc $?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Đây là những ví dụ giả định, nhưng tiềm năng không tồn tại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.