Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 104 additions & 28 deletions src/operators/inspect_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,31 @@
*
*/

/*
* ModSecurity, http://www.modsecurity.org/
* Copyright (c) 2015 - 2021 Trustwave Holdings, Inc.
*
* Licensed under the Apache License, Version 2.0
*/

Comment on lines +16 to +22
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file already contains the full ModSecurity/Apache 2.0 license header at the top. This additional shorter license block is redundant and can create confusion/inconsistency across the codebase; please remove the duplicated header and keep the existing canonical one only.

Suggested change
/*
* ModSecurity, http://www.modsecurity.org/
* Copyright (c) 2015 - 2021 Trustwave Holdings, Inc.
*
* Licensed under the Apache License, Version 2.0
*/

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is true, it's a bit weird to see two licenses block :).

Please remove this one.

#include "src/operators/inspect_file.h"

#include <stdio.h>

#include <string>
#include <iostream>
#include <sstream>
#include <array>
#include <vector>

#include "src/operators/operator.h"
#include "src/utils/system.h"

#ifdef WIN32
#include "src/compat/msvc.h"
#else
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

namespace modsecurity {
Expand Down Expand Up @@ -52,40 +65,103 @@ bool InspectFile::init(const std::string &param2, std::string *error) {
return true;
}


bool InspectFile::evaluate(Transaction *transaction, const std::string &str) {
if (m_isScript) {
return m_lua.run(transaction, str);
} else {
FILE *in;
char buff[512];
std::stringstream s;
std::string res;
std::string openstr;

openstr.append(m_param);
openstr.append(" ");
openstr.append(str);
if (!(in = popen(openstr.c_str(), "r"))) {
return false;
}

while (fgets(buff, sizeof(buff), in) != NULL) {
s << buff;
}

pclose(in);

res.append(s.str());
if (res.size() > 1 && res[0] != '1') {
return true; /* match */
}

/* no match */
}

#ifndef WIN32
/*
* SECURITY HARDENING:
* Replace shell-based popen() execution with fork()+execvp()
* to avoid shell interpretation while preserving behavior.
*/

std::array<int, 2> pipefd{};
if (pipe(pipefd.data()) == -1) {
return false;
}

pid_t pid = fork();
if (pid == -1) {
close(pipefd[0]);
close(pipefd[1]);
return false;
}

if (pid == 0) {
// Child process
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);

// Create mutable copies (avoid const_cast)
std::string param_copy = m_param;
std::string str_copy = str;

std::vector<char*> argv;
argv.push_back(param_copy.data());
argv.push_back(str_copy.data());
argv.push_back(nullptr);

execvp(argv[0], argv.data());
Comment on lines +98 to +107
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In init(), the script/executable path is resolved into m_file via utils::find_resource(), but the non-Lua execution path still executes m_param. This can execute a different binary (PATH lookup) or fail when the resource was found relative to the config. Prefer executing the resolved path (m_file) and use execv() (or execve()) rather than execvp() to avoid PATH ambiguity.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider to change this logic as Copilot suggests.


_exit(1); // exec failed
}

// Parent process
close(pipefd[1]);

std::stringstream s;
std::array<char, 512> buff{};
ssize_t count;

while ((count = read(pipefd[0], buff.data(), buff.size())) > 0) {
s.write(buff.data(), count);
}

close(pipefd[0]);
waitpid(pid, nullptr, 0);

Comment on lines +119 to +125
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read()/waitpid() handling should be robust against EINTR and should check the child exit status. As written, an interrupted read() will stop early and waitpid() ignores failures/status, so exec failures or signal termination may be silently treated as 'no match'. Consider retrying on EINTR and returning false (and/or logging) when the child exits non-zero.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider this one too.

if (const std::string res = s.str();
res.size() > 1 && res[0] != '1') {
return true;
}

return false;

Comment on lines +126 to +132
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structured if statement here is mis-indented (and the closing brace alignment is off), which is likely to trip the repo's coding-style checks and makes the control flow harder to read. Please reformat this block so the condition, braces, and returns follow the surrounding indentation conventions.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a formatting issue, but please use the correct indentation.

#else
/*
* Windows fallback: preserve existing behavior
*/
FILE *in;
std::array<char, 512> buff{};
std::stringstream s;
std::string res;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Windows fallback branch, std::string res; is now unused because a new res is declared in the structured if statement below (shadowing the earlier variable). This can introduce unused-variable warnings; remove the outer res declaration or reuse it instead of redeclaring.

Suggested change
std::string res;

Copilot uses AI. Check for mistakes.
std::string openstr;

openstr.append(m_param);
openstr.append(" ");
openstr.append(str);

if (!(in = popen(openstr.c_str(), "r"))) {
return false;
}

while (fgets(buff.data(), buff.size(), in) != NULL) {
s << buff.data();
}

pclose(in);

if (const std::string res = s.str();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Copilot review mentions a variable with the same name: res - it's shadowed here.

And that's the problem of cppcheck now (see):

2026-03-05T19:58:57.7181420Z warning: src/operators/inspect_file.cc,157,style,shadowVariable,Local variable 'res' shadows outer variable
2026-03-05T19:58:57.7307690Z warning: src/operators/inspect_file.cc,140,style,unusedVariable,Unused variable: res

(use CTRL-F and warning to find these).

res.size() > 1 && res[0] != '1') {
return true;
}

return false;
Comment on lines +157 to +162
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This structured if statement is also mis-indented in the Windows fallback path. Please reformat it to match the surrounding style (condition alignment, brace placement, and indentation of the return).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also an indentation request change.

#endif
}

} // namespace operators
} // namespace modsecurity
Loading