对C++变长参数中的字符串进行转义

使用过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。

Guess you like

Origin blog.csdn.net/witton/article/details/114022443