Tại sao gets()
nguy hiểm
Con sâu internet đầu tiên ( Worm Internet Internet ) đã trốn thoát khoảng 30 năm trước (1988-11 / 02), và nó đã sử dụng gets()
và tràn bộ đệm như một trong những phương pháp truyền từ hệ thống này sang hệ thống khác. Vấn đề cơ bản là hàm không biết bộ đệm lớn đến mức nào, vì vậy nó tiếp tục đọc cho đến khi tìm thấy dòng mới hoặc gặp EOF và có thể vượt quá giới hạn của bộ đệm được cung cấp.
Bạn nên quên bạn đã từng nghe rằng gets()
tồn tại.
Tiêu chuẩn C11 ISO / IEC 9899: 2011 đã bị loại bỏ gets()
như một chức năng tiêu chuẩn, đó là A Good Thing ™ (nó được đánh dấu chính thức là 'lỗi thời' và 'không dùng nữa' trong ISO / IEC 9899: 1999 / Cor.3: 2007 - Bản sửa lỗi kỹ thuật 3 cho C99, và sau đó loại bỏ trong C11). Đáng buồn thay, nó sẽ tồn tại trong các thư viện trong nhiều năm (có nghĩa là 'thập kỷ') vì lý do tương thích ngược. Nếu nó tùy thuộc vào tôi, việc thực hiện gets()
sẽ trở thành:
char *gets(char *buffer)
{
assert(buffer != 0);
abort();
return 0;
}
Dù rằng mã của bạn sẽ bị sập dù thế nào, sớm hay muộn, tốt hơn là nên giải quyết vấn đề sớm hơn là muộn hơn. Tôi đã sẵn sàng để thêm một thông báo lỗi:
fputs("obsolete and dangerous function gets() called\n", stderr);
Các phiên bản hiện đại của hệ thống biên dịch Linux tạo cảnh báo nếu bạn liên kết gets()
- và cả một số chức năng khác cũng có vấn đề về bảo mật ( mktemp()
, khắc).
Các lựa chọn thay thế cho gets()
fget ()
Như mọi người khác đã nói, sự thay thế chính tắc gets()
được fgets()
chỉ định stdin
là luồng tệp.
char buffer[BUFSIZ];
while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
...process line of data...
}
Điều mà không ai khác đề cập là gets()
không bao gồm dòng mới nhưng fgets()
không có. Vì vậy, bạn có thể cần phải sử dụng một trình bao bọc xung quanh fgets()
để xóa dòng mới:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
return buffer;
}
return 0;
}
Hoặc tốt hơn:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
buffer[strcspn(buffer, "\n")] = '\0';
return buffer;
}
return 0;
}
Ngoài ra, như quán cà phê chỉ ra trong một bình luận và paxdiablo hiển thị trong câu trả lời của anh ấy, với fgets()
bạn có thể có dữ liệu còn sót lại trên một dòng. Mã trình bao bọc của tôi để dữ liệu đó được đọc lần sau; bạn có thể dễ dàng sửa đổi nó để ngấu nghiến phần còn lại của dòng dữ liệu nếu bạn thích:
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
else
{
int ch;
while ((ch = getc(fp)) != EOF && ch != '\n')
;
}
Vấn đề còn lại là làm thế nào để báo cáo ba trạng thái kết quả khác nhau - EOF hoặc lỗi, đọc dòng và không bị cắt, và đọc một phần dòng nhưng dữ liệu đã bị cắt ngắn.
Vấn đề này không xảy ra gets()
vì nó không biết bộ đệm của bạn kết thúc ở đâu và vui vẻ vượt quá cuối, phá hỏng bố cục bộ nhớ được chăm sóc đẹp mắt của bạn, thường làm rối ngăn xếp trở lại ( tràn ngăn xếp ) nếu bộ đệm được cấp phát ngăn xếp hoặc chà đạp lên thông tin điều khiển nếu bộ đệm được phân bổ động hoặc sao chép dữ liệu qua các biến toàn cục (hoặc mô-đun) quý giá khác nếu bộ đệm được phân bổ tĩnh. Không ai trong số này là một ý tưởng hay - họ mô tả cụm từ 'hành vi không xác định`.
Ngoài ra còn có TR 24731-1 (Báo cáo kỹ thuật từ Ủy ban tiêu chuẩn C) cung cấp các lựa chọn thay thế an toàn hơn cho nhiều chức năng, bao gồm gets()
:
§6.5.4.1 gets_s
Hàm
Tóm tắc
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
Hạn chế thời gian chạy
s
sẽ không phải là một con trỏ null n
sẽ không bằng 0 và không lớn hơn RSIZE_MAX. Một ký tự dòng mới, cuối tập tin hoặc lỗi đọc sẽ xảy ra trong khi đọc các
n-1
ký tự từ stdin
. 25)
3 Nếu có vi phạm ràng buộc thời gian chạy, s[0]
được đặt thành ký tự null và các ký tự được đọc và loại bỏ stdin
cho đến khi một ký tự dòng mới được đọc, hoặc kết thúc tệp hoặc xảy ra lỗi đọc.
Sự miêu tả
4 gets_s
Hàm đọc tối đa một ít hơn số lượng ký tự được chỉ định n
từ luồng được trỏ tới stdin
, vào mảng được chỉ bởi s
. Không có ký tự bổ sung nào được đọc sau ký tự dòng mới (bị loại bỏ) hoặc sau khi kết thúc tệp. Ký tự dòng mới bị loại bỏ không được tính vào số lượng ký tự được đọc. Một ký tự null được viết ngay sau ký tự cuối cùng được đọc vào mảng.
5 Nếu gặp phải tập tin cuối và không có ký tự nào được đọc vào mảng hoặc nếu xảy ra lỗi đọc trong quá trình thao tác, thì s[0]
được đặt thành ký tự null và các thành phần khác của các s
giá trị không xác định.
Đề nghị thực hành
6 fgets
Hàm cho phép các chương trình được viết đúng để xử lý một cách an toàn các dòng đầu vào quá lâu để lưu trữ trong mảng kết quả. Nói chung, điều này đòi hỏi người gọi fgets
phải chú ý đến sự hiện diện hoặc vắng mặt của một ký tự dòng mới trong mảng kết quả. Cân nhắc sử dụng fgets
(cùng với bất kỳ xử lý cần thiết nào dựa trên các ký tự dòng mới) thay vì
gets_s
.
25) Các gets_s
chức năng, không giống như gets
, làm cho nó một sự vi phạm runtime-hạn chế cho một dòng đầu vào làm tràn bộ đệm để lưu trữ nó. Không giống như fgets
, gets_s
duy trì mối quan hệ một-một giữa các dòng đầu vào và các cuộc gọi thành công gets_s
. Các chương trình sử dụng gets
mong đợi một mối quan hệ như vậy.
Trình biên dịch Microsoft Visual Studio triển khai xấp xỉ với tiêu chuẩn TR 24731-1, nhưng có sự khác biệt giữa các chữ ký được Microsoft triển khai và các chữ ký trong TR.
Tiêu chuẩn C11, ISO / IEC 9899-2011, bao gồm TR24731 trong Phụ lục K là một phần tùy chọn của thư viện. Thật không may, nó hiếm khi được thực hiện trên các hệ thống giống như Unix.
getline()
- POSIX
POSIX 2008 cũng cung cấp một sự thay thế an toàn để gets()
gọi getline()
. Nó phân bổ không gian cho dòng một cách linh hoạt, vì vậy cuối cùng bạn cần giải phóng nó. Do đó, nó loại bỏ giới hạn về độ dài dòng. Nó cũng trả về độ dài của dữ liệu đã đọc, hoặc -1
(và không EOF
!), Điều đó có nghĩa là các byte rỗng trong đầu vào có thể được xử lý một cách đáng tin cậy. Ngoài ra còn có một biến thể 'chọn ký tự phân cách một ký tự của riêng bạn được gọi là getdelim()
; điều này có thể hữu ích nếu bạn đang xử lý đầu ra từ find -print0
nơi kết thúc của tên tệp được đánh dấu bằng ký tự ASCII NUL '\0'
.
gets()
Buffer_overflow_attack