• 在写windows C++代码的时候,从代码安全角度考虑,我们应该注意什么?


            在写windows C++代码的时候,从代码安全角度考虑,我们应该注意什么?分别是:输入验证、内存管理、错误处理、并发和线程安全、使用安全的API、避免使用不安全的函数、最小权限原则。

    一、输入验证

    1. 用户输入验证

    1. #include
    2. #include
    3. #include
    4. int main() {
    5. int age;
    6. while (true) {
    7. std::cout << "请输入您的年龄: ";
    8. if (std::cin >> age && age > 0 && age < 130) {
    9. break;
    10. } else {
    11. std::cout << "无效输入,请重新输入。\n";
    12. std::cin.clear(); // 清除失败的输入
    13. std::cin.ignore(std::numeric_limits::max(), '\n'); // 忽略错误输入之后的字符
    14. }
    15. }
    16. std::cout << "输入的年龄是: " << age << std::endl;
    17. return 0;
    18. }

    2. 文件读取验证

            当从文件中读取数据时,您需要验证文件是否存在,是否可以被打开,以及读取的数据是否符合预期格式。例如,读取一个存储数字的文件:

    1. #include
    2. #include
    3. #include
    4. int main() {
    5. std::ifstream file("numbers.txt");
    6. std::vector<int> numbers;
    7. int number;
    8. if (!file) {
    9. std::cerr << "无法打开文件" << std::endl;
    10. return 1;
    11. }
    12. while (file >> number) {
    13. numbers.push_back(number);
    14. }
    15. if (!file.eof()) {
    16. std::cerr << "文件读取过程中发生错误" << std::endl;
    17. return 1;
    18. }
    19. std::cout << "读取到的数字有: ";
    20. for (int num : numbers) {
    21. std::cout << num << " ";
    22. }
    23. std::cout << std::endl;
    24. return 0;
    25. }

    3. 网络数据接收验证

            在处理网络通信时,接收到的数据应该经过严格验证。例如,如果您的程序是一个服务器,它可能需要验证从客户端接收到的消息格式是否正确:

    1. // 假设这是一个简单的TCP服务器接收数据的代码片段
    2. char buffer[1024];
    3. int receivedBytes = recv(clientSocket, buffer, 1024, 0);
    4. if (receivedBytes > 0) {
    5. // 验证接收到的数据
    6. if (isValidMessage(buffer, receivedBytes)) {
    7. // 处理消息
    8. } else {
    9. std::cerr << "接收到无效的消息格式" << std::endl;
    10. }
    11. }

    二、内存管理

    1. 使用裸指针管理动态内存(传统方法)

    1. int* ptr = new int(10); // 分配内存
    2. // 使用 ptr...
    3. delete ptr; // 释放内存
    4. ptr = nullptr; // 防止悬挂指针

            在这个例子中,必须确保在 ptr 不再需要时释放分配的内存,并将指针设为 nullptr 以避免悬挂指针。这种方法容易出错,因为需要手动管理内存。

    2. 使用智能指针管理动态内存(现代方法)

            C++11 引入的智能指针(如 std::unique_ptrstd::shared_ptr)自动管理内存,可以减少内存泄漏的风险。

    1. #include
    2. std::unique_ptr<int> ptr(new int(10));
    3. // 使用 ptr...
    4. // 不需要手动释放内存,当 ptr 离开作用域时,内存会自动被释放

    3. 防止数组越界

    4. 避免内存泄漏

    1. void function() {
    2. int* ptr = new int(10); // 分配内存
    3. // 函数的其他操作...
    4. delete ptr; // 释放内存
    5. }

            如果函数中有多个返回路径或可能抛出异常,使用智能指针来保证内存在任何情况下都能被正确释放。

    三、错误处理机制

    1. 使用异常处理机制

            C++ 提供了异常处理机制,允许在检测到错误时抛出异常,并在上层代码中捕获和处理这些异常。

    1. #include
    2. #include
    3. double divide(double a, double b) {
    4. if (b == 0) {
    5. throw std::invalid_argument("除数不能为0");
    6. }
    7. return a / b;
    8. }
    9. int main() {
    10. try {
    11. double result = divide(10.0, 0.0);
    12. std::cout << "结果: " << result << std::endl;
    13. } catch (const std::invalid_argument& e) {
    14. std::cerr << "捕获到错误: " << e.what() << std::endl;
    15. }
    16. return 0;
    17. }

    2. 使用返回值进行错误指示

            在某些情况下,使用返回值来指示错误是更加合适的。这种方法常用于 C 风格的代码或者需要保持与旧代码的兼容性。

    1. #include
    2. bool safeDivide(double a, double b, double& result) {
    3. if (b == 0) {
    4. return false; // 错误指示
    5. }
    6. result = a / b;
    7. return true; // 操作成功
    8. }
    9. int main() {
    10. double result;
    11. if (!safeDivide(10.0, 0.0, result)) {
    12. std::cerr << "错误:除数不能为0" << std::endl;
    13. } else {
    14. std::cout << "结果: " << result << std::endl;
    15. }
    16. return 0;
    17. }

    3. 使用错误码

    四、最小权限原则

            最小权限原则(Principle of Least Privilege, PoLP)是一种安全设计原则,意在确保程序、进程或用户仅具有完成其任务所必需的最小权限集。在 Windows C++ 编程中,这通常涉及到操作系统资源的访问和管理。以下是一些应用最小权限原则的例子:

    1. 运行权限限制

            当开发一个应用程序时,确保它以最低必要的权限运行。例如,如果您的程序仅需读取文件,不应请求或具有写入文件的权限。

    1. // 以只读方式打开文件
    2. std::ifstream file("example.txt");
    3. if (!file) {
    4. std::cerr << "无法打开文件,权限不足或文件不存在" << std::endl;
    5. // 错误处理
    6. }

    2. 数据库访问

            当您的程序需要与数据库交互时,为数据库用户分配只能执行其需求的操作的最小权限。例如,如果程序只需要从数据库读取数据,那么数据库用户不应该具有写入、修改或删除数据的权限。

    1. // 假设这是一个数据库连接代码片段
    2. // 这个数据库用户只有读取数据的权限
    3. const char* connectionString = "User=ReadOnlyUser;Password=example;...";

    3. 网络服务权限

            如果您的程序是一个网络服务,确保它在一个受限的环境中运行,例如在低权限的用户账户下运行,避免在需要管理员权限的账户下运行。

    4. 文件和目录权限

            当您的程序需要创建文件或目录时,设置合理的访问控制列表(ACL),以确保只有必要的用户或程序有权访问这些资源。

    五、并发和线程安全

            在并发编程中,线程安全是一个核心考虑因素。线程安全的代码可以在多线程环境中安全地被多个线程同时执行,而不会导致数据损坏或不一致的状态。以下是一些并发和线程安全的例子:

    1. 使用互斥锁

            互斥锁(mutex)是保护共享资源免受多个线程同时访问的一种方法。

    1. #include
    2. #include
    3. #include
    4. std::mutex mtx; // 全局互斥锁
    5. int sharedData = 0; // 共享数据
    6. void increment() {
    7. mtx.lock(); // 获取锁
    8. ++sharedData; // 修改共享数据
    9. mtx.unlock(); // 释放锁
    10. }
    11. int main() {
    12. std::thread t1(increment);
    13. std::thread t2(increment);
    14. t1.join();
    15. t2.join();
    16. std::cout << "共享数据的值: " << sharedData << std::endl;
    17. return 0;
    18. }

    2. 使用条件变量

            条件变量是一种允许线程等待特定条件的同步机制。

    1. #include
    2. #include
    3. #include
    4. #include
    5. std::mutex mtx;
    6. std::condition_variable cv;
    7. bool ready = false;
    8. void printNumber(int num) {
    9. std::unique_lock lck(mtx);
    10. while (!ready) {
    11. cv.wait(lck);
    12. }
    13. std::cout << "Number: " << num << std::endl;
    14. }
    15. void setReady() {
    16. {
    17. std::lock_guard lck(mtx);
    18. ready = true;
    19. }
    20. cv.notify_all();
    21. }
    22. int main() {
    23. std::thread t1(printNumber, 1);
    24. std::thread t2(printNumber, 2);
    25. setReady();
    26. t1.join();
    27. t2.join();
    28. return 0;
    29. }

            在这个例子中,两个线程等待 ready 变量变为 true。一旦 setReady 函数被调用,条件变量通知所有等待的线程继续执行。

    3. 使用原子操作

            原子操作是不可分割的操作,可以在多线程环境中安全地执行,不需要额外的同步机制。

    1. #include
    2. #include
    3. #include
    4. std::atomic<int> count(0); // 原子变量
    5. void increment() {
    6. for (int i = 0; i < 10000; ++i) {
    7. count++; // 原子操作
    8. }
    9. }
    10. int main() {
    11. std::thread t1(increment);
    12. std::thread t2(increment);
    13. t1.join();
    14. t2.join();
    15. std::cout << "计数: " << count << std::endl;
    16. return 0;
    17. }

            在这个例子中,两个线程同时增加一个计数器。由于 count 是一个原子类型,每次增加操作都是线程安全的。

    六、使用安全的函数(Windows api)

            这里只需注意需要使用安全的。

    1. 字符串复制

    1. char source[] = "Hello World";
    2. char dest[11];
    3. strcpy(dest, source); // 不安全,没有边界检查
    4. char source[] = "Hello World";
    5. char dest[11];
    6. strncpy(dest, source, sizeof(dest) - 1); // 安全,指定最大复制长度
    7. dest[sizeof(dest) - 1] = '\0'; // 确保字符串以空字符结尾

    strcpy 的问题

      strcpy 函数用于将一个字符串复制到另一个字符串。它继续复制字符直到遇到源字符串的空终止字符('\0')。这个函数的问题在于它不考虑目标字符串的大小。如果源字符串长度超过了目标字符串的容量,strcpy 会继续写入,导致超出目标数组的边界,造成缓冲区溢出。这种溢出可能覆盖其他重要的内存数据,引起程序崩溃或安全漏洞。

            char source[] = "这个字符串很长,超过目标缓冲区的大小"; char dest[10]; // 只有10个字符的空间 strcpy(dest, source); // 危险:源字符串长度超过了dest的容量

            在这个例子中,dest 只能容纳9个字符和一个空终止符,但 source 的长度远远超过这个限制,导致 dest 的边界被溢出。

    strncpy 的相对安全性

      strncpy 函数也是用于复制字符串,但它接受一个额外的参数来指定最大复制的字符数。这个函数在到达最大字符数或遇到源字符串的空终止符时停止复制。这有助于防止超出目标数组的边界,如果正确使用的话。

    1. char source[] = "这个字符串很长,可能超过目标缓冲区的大小";
    2. char dest[10];
    3. strncpy(dest, source, sizeof(dest) - 1); // 复制最多9个字符
    4. dest[sizeof(dest) - 1] = '\0'; // 确保有空终止符

    strcpy 的问题

    strcpy 函数用于将一个字符串复制到另一个字符串。它继续复制字符直到遇到源字符串的空终止字符('\0')。这个函数的问题在于它不考虑目标字符串的大小。如果源字符串长度超过了目标字符串的容量,strcpy 会继续写入,导致超出目标数组的边界,造成缓冲区溢出。这种溢出可能覆盖其他重要的内存数据,引起程序崩溃或安全漏洞。

    strncpy 的相对安全性

    strncpy 函数也是用于复制字符串,但它接受一个额外的参数来指定最大复制的字符数。这个函数在到达最大字符数或遇到源字符串的空终止符时停止复制。这有助于防止超出目标数组的边界,如果正确使用的话。

    例子:使用 strncpy 防止缓冲区溢出

    2. 字符串连接

    1. char str[10] = "Hello";
    2. strcat(str, " World"); // 不安全,可能导致缓冲区溢出
    3. char str[15] = "Hello";
    4. strncat(str, " World", sizeof(str) - strlen(str) - 1); // 安全,限制最大添加长度

    3. 格式化字符串

    1. char buffer[50];
    2. sprintf(buffer, "Value: %d", 123); // 不安全,没有边界检查
    3. char buffer[50];
    4. snprintf(buffer, sizeof(buffer), "Value: %d", 123); // 安全,限制最大写入长度

            虽然 strncpystrcpy 安全,但它仍然需要小心使用。如果最大复制长度小于源字符串长度,strncpy 不会自动添加空终止符,可能导致目标字符串没有正确终止。因此,使用 strncpy 时,程序员需要确保目标字符串有足够的空间,并在需要时手动添加空终止符。

     

  • 相关阅读:
    Vue事件修饰符
    网课题库接口API—小白专用
    自动驾驶仿真:VTD调用罗技 G923方向盘(Linux环境)
    【软考】PV 操作
    [附源码]SSM计算机毕业设计学生档案管理系统JAVA
    csp 202203-2 出行计划
    前端中的同步和异步任务详细说明
    记一次stm32开发的环境搭建过程
    NLP论文解读:无需模板且高效的语言微调模型(上)
    .NET开源强大、易于使用的缓存框架 - FusionCache
  • 原文地址:https://blog.csdn.net/weixin_44120785/article/details/134486584