使用过SQL的读者应该都知道SQL存在注入的可能,即没有严格检查用户输入数据的合法性。这里不讨论SQL的注入以及防止注入,只谈一下在C++中对将要执行的SQL中的字符串参数进行转义。
最近项目中遇到一个SQL相关的问题,用户在客户端输入了一个字符串数据,这个字符串数据需要保存到数据库,但是恰好有一个用户输入了一个带引号的字符串数据,导致服务器在执行SQL语句进行存储的时候出现语法错误。
我们在实际项目中,应该会封装一个函数来对SQL语句进行格式化,比如:
string FormatSQL(const char* format, ...)
{
char buf[8192];
va_list args;
va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
return buf;
}
void testSQL()
{
const char* name = "witton";
const char* desc = "'hello";
string SQL = FormatSQL("insert tmp(name, desc) values('%s', '%s')", name, desc);
printf("%s\n", SQL.c_str());
}
这个SQL语句最后会变成:
insert tmp(name, desc) values('witton', ''hello')
可以看到由于没有进行转义desc中的单引号,所以导致SQL出现语法错误。
MySQL的C API提供了两个函数来进行字符串转义:
unsigned long STDCALL mysql_escape_string(char *to,const char *from,
unsigned long from_length);
unsigned long STDCALL mysql_real_escape_string(MYSQL *mysql,
char *to,const char *from,
unsigned long length);
这两个函数推荐使用mysql_real_escape_string来进行转义,因为mysql_real_escape_string会要求传入MYSQL指针,该数据结构中有相应的字符集编码,转义时可以根据设定的字符集编码来进行转义。
回到前面的FormatSQL函数,它是使用的标准C的方式来格式化的,如果有很多字符串参数,就需要对每一个字符串参数写一行代码进行转义,比如前面的testSQL函数,就需要写成如下所示代码:
void testSQL()
{
const char* name = "witton";
const char* desc = "'hello";
char bufName[256];
char bufDesc[256];
mysql_real_escape_string(mysql, bufName, name, strlen(name));
mysql_real_escape_string(mysql, bufDesc, desc, strlen(desc));
string SQL = FormatSQL("insert tmp(name, desc) values('%s', '%s')", bufName, bufDesc);
printf("%s\n", SQL.c_str());
}
如果是新写的项目,在最开始就注意这些细节还好,如果是老项目,原来没有做这些,一个一个挨着去修改,显得非常麻烦,我现在所在的项目就是这样一种情况。
有没有办法不改变现有的对FormatSQL函数的调用方式,又能够在调用FormatSQL的过程中自动对参数中字符串进行转义呢?
答案就是对FormatSQL进行修改,使用C++的变长模板参数,在匹配到字符串时自动调用转义函数。
先来看看C++11引入的变长模板参数:
template<typename... Args> class VarTemplate;
也可以使用在模板函数上,标准 C 中的 printf 函数, 虽然也能达成变长形参的调用,但并非类型安全。 而 C++11 除了能定义类型安全的变长参数函数外, 还可以使类似 printf 的函数能自然地处理自定义类型的对象。 除了在模板参数中能使用 … 表示变长模板参数外, 函数参数也使用同样的表示法代表变长参数, 例如前面的FormatSQL函数可以写成:
template<typename... Args>
void FormatSQL(const char* format, Args&& ... args);
由于在调用FormatSQL时通常会有常量作为参数,所以变长参数在传入时使用的右值引用。
我们先来定义转义函数,该函数在遇到字符串类型时则调用转义函数,否则不作转义:
template<typename Arg>
Arg& EscapeArg(Arg& arg)
{
//非字符串不作转义
return arg;
}
// const char arg[]参数匹配的字符串常量,比如FormatSQL("%s", "witton")中的"witton"
const char* EscapeArg(const char arg[])
{
size_t len = strlen(arg);
char* buf = new char[len * 2];
mysql_escape_string(buf, arg, (unsigned long)len);
return buf;
}
// string参数匹配的是string变量比如:
// string str = "witton";
// FormatSQL("%s", str);
const char* EscapeArg(string& str)
{
return EscapeArg(str.c_str());
}
转义函数写好了,转义好了还需要把每个转义后的参数连接起来再格式化出来:
void Concat(string& str, size_t len, const char* format, ...)
{
str.resize(len);
char* buf = (char*)str.c_str();
va_list ap;
va_start(ap, format);
vsnprintf(buf, len, format, ap);
va_end(ap);
}
为了提高性能,这里要求外部传入一个string来存入结果,这个string由外部来决定长度,以避免空间不足。
接下来就是实现FormatSQL函数,由于该函数是一个变长模板函数,在使用中需要对每一个参数调用EscapeArg函数来进行转义,然后再把转义后的结果依次传入Concat进行格式化。
变长模板参数Args … args在使用过程中需要展开,展开方式有两种:
一种是递归方式,比如:
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
另外一种是使用逗号表达式的方式,比如:
template<typename OS,typename T> void outstr(OS& o,T t)
{
o << t;
}
template<typename... ARG> auto argcat(ARG... arg)->string
{
ostringstream os;
int arr[] = { (outstr(os,arg),0)...};
return os.str();
}
int main()
{
cout << argcat(1, 2.3, "my name is", '\t',"lc") << endl;
return 0;
}
由于需要把转义后的结果依次传入Concat进行格式化,所以最好的方式是使用类逗号扩展方式:
template<typename ... Args>
string FormatSQL(const char* fmt, Args&& ... args)
{
size_t len = 8192;
string str;
Concat(str, len, fmt, EscapeArg(args)...);
return str;
}
这里就基本上完成了要求。细心的读者可能已经发现了问题,就是在调用EscapeArg进行字符串扩展的时候分配了内存,但是没有释放,会造成内存泄漏。所以我们需要把分配的内存地址保存下来,待Concat格式化完成后进行释放。另外,在Concat格式化时要求输入一个长度len,这里是写的固定长度,如果超过则会出问题,所以我们还需要根据参数计算一个合适长度。计算工作就一同交给EscapeArg来做。
下面就直接把最终代码附上:
template<typename Arg>
Arg& EscapeArg(size_t& len, vector<char*>& vct, Arg& arg)
{
len += sizeof(Arg) * 8;
return arg;
}
const char* EscapeArg(size_t& Len, vector<char*>& vct, const char arg[])
{
size_t len = strlen(arg);
char* buf = new char[len * 2];
Len += mysql_escape_string(buf, arg, (unsigned long)len);
vct.push_back(buf);
return buf;
}
const char* EscapeArg(size_t& Len, vector<char*>& vct, string& str)
{
return EscapeArg(Len, vct, str.c_str());
}
void Concat(string& str, size_t len, const char* format, ...)
{
str.resize(len);
char* buf = (char*)str.c_str();
va_list ap;
va_start(ap, format);
vsnprintf(buf, len, format, ap);
va_end(ap);
}
template<typename ... Args>
string FormatSQL(const char* fmt, Args&& ... args)
{
vector<char*> vct;
size_t len = strlen(fmt);
string str;
Concat(str, len, fmt, EscapeArg(len, vct, args)...);
for (auto iter : vct)
delete[] iter;
return str;
}
该代码在VS2015、GCC 4.9.3、Clang 9下编译测试通过。GCC以及Clang需要添加参数-std=c++11。