Skip to content

Commit 3f35c2c

Browse files
fix(redis): avoid formatted command overread (#3570)
Retry formatted values in dynamically sized storage when vsnprintf reports an output larger than the stack buffer. Add a vector-based RedisRequest component overload and use it for authentication and cluster command construction while retaining the existing format APIs. Add coverage for wide numeric formatting, binary-safe components, and AUTH/SELECT credential encoding. Verified with RedisCommandFormatTest.* and RedisClusterChannelTest.basic_routing_and_multi_key_commands.
1 parent 2621a26 commit 3f35c2c

8 files changed

Lines changed: 80 additions & 13 deletions

File tree

‎docs/cn/redis_client.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ bool AddCommandByComponents(const butil::StringPiece* components, size_t n);
128128
129129
> 注意,AddCommand和AddCommandV的fmt参数如果设置错误,有可能导致程序crash或者数据泄露,请谨慎设置。不应将受用户输入影响的内容作为fmt参数进行调用!
130130
131-
AddCommandByComponents类似hiredis中的redisCommandArgv,用户通过数组指定命令中的每一个部分。这个方法对AddCommand和AddCommandV可能发生的转义问题免疫,且效率最高。如果你在使用AddCommand和AddCommandV时出现了“Unmatched quote”,“无效格式”等问题且无法定位,可以试下这个方法。
131+
AddCommandByComponents类似hiredis中的redisCommandArgv,用户可通过数组或`std::vector<butil::StringPiece>`指定命令中的每个部分。这个方法对AddCommand和AddCommandV可能发生的转义问题免疫。如果你在使用AddCommand和AddCommandV时出现“Unmatched quote”“无效格式”等问题且无法定位,可以试下这个方法。
132132
133133
如果AddCommand\*失败,后续的AddCommand\*和CallMethod都会失败。一般来说不用判AddCommand*的结果,失败后自然会通过RPC失败体现出来。
134134

‎docs/en/redis_client.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ Formatting is compatible with hiredis, namely `%b` corresponds to binary data (p
129129
130130
> Note that if the `fmt` parameter of `AddCommand` and `AddCommandV` is set incorrectly, it may cause the program to crash or lead to data leakage. Please set it with caution. Should not use user input-influenced content as the `fmt` parameter!
131131
132-
`AddCommandByComponents` is similar to `redisCommandArgv` in hiredis. Users specify each part of the command in an array, which is immune to escaping issues often occurring in `AddCommand` and `AddCommandV`. If you encounter errors such as "Unmatched quote" or "invalid format" when using `AddCommand` and `AddCommandV`, try this method.
132+
`AddCommandByComponents` is similar to `redisCommandArgv` in hiredis. Users specify each part of the command in an array or `std::vector<butil::StringPiece>`, which is immune to escaping issues often occurring in `AddCommand` and `AddCommandV`. If you encounter errors such as "Unmatched quote" or "invalid format" when using `AddCommand` and `AddCommandV`, try this method.
133133
134134
If `AddCommand*` fails, subsequent `AddCommand*` and `CallMethod` also fail. In general, there is no need to check return value of `AddCommand*`, since the RPC fails directly anyway.
135135

‎src/brpc/policy/redis_authenticator.cpp‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,30 @@
1717

1818
#include "brpc/policy/redis_authenticator.h"
1919

20-
#include "butil/base64.h"
2120
#include "butil/iobuf.h"
22-
#include "butil/string_printf.h"
23-
#include "butil/sys_byteorder.h"
24-
#include "brpc/redis_command.h"
21+
#include "brpc/redis.h"
2522

2623
namespace brpc {
2724
namespace policy {
2825

2926
int RedisAuthenticator::GenerateCredential(std::string* auth_str) const {
30-
butil::IOBuf buf;
27+
RedisRequest request;
3128
if (!passwd_.empty()) {
32-
brpc::RedisCommandFormat(&buf, "AUTH %s", passwd_.c_str());
29+
const std::vector<butil::StringPiece> components = {"AUTH", passwd_};
30+
if (!request.AddCommandByComponents(components)) {
31+
return -1;
32+
}
3333
}
3434
if (db_ >= 0) {
35-
brpc::RedisCommandFormat(&buf, "SELECT %d", db_);
35+
const std::string db = std::to_string(db_);
36+
const std::vector<butil::StringPiece> components = {"SELECT", db};
37+
if (!request.AddCommandByComponents(components)) {
38+
return -1;
39+
}
40+
}
41+
butil::IOBuf buf;
42+
if (!request.SerializeTo(&buf)) {
43+
return -1;
3644
}
3745
*auth_str = buf.to_string();
3846
return 0;

‎src/brpc/redis.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ bool RedisRequest::AddCommandByComponents(const butil::StringPiece* components,
120120
}
121121
}
122122

123+
bool RedisRequest::AddCommandByComponents(
124+
const std::vector<butil::StringPiece>& components) {
125+
return AddCommandByComponents(components.data(), components.size());
126+
}
127+
123128
bool RedisRequest::AddCommandWithArgs(const char* fmt, ...) {
124129
if (_has_error) {
125130
return false;

‎src/brpc/redis.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#define BRPC_REDIS_H
2121

2222
#include <unordered_map>
23+
#include <vector>
2324

2425
#include "brpc/destroyable.h"
2526
#include "brpc/nonreflectable_message.h"
@@ -66,6 +67,8 @@ class RedisRequest : public NonreflectableMessage<RedisRequest> {
6667
// butil::StringPiece components[] = { "set", "key", "value" };
6768
// request.AddCommandByComponents(components, arraysize(components));
6869
bool AddCommandByComponents(const butil::StringPiece* components, size_t n);
70+
// Convenient when the components are already held in a vector.
71+
bool AddCommandByComponents(const std::vector<butil::StringPiece>& components);
6972

7073
// Add a command with variadic args to this request.
7174
// The reason that adding so many overloads rather than using ... is that

‎src/brpc/redis_cluster.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ bool RedisClusterChannel::SendToEndpoint(const std::string& endpoint,
666666
for (size_t i = 0; i < args.size(); ++i) {
667667
components.push_back(args[i]);
668668
}
669-
if (!request.AddCommandByComponents(&components[0], components.size())) {
669+
if (!request.AddCommandByComponents(components)) {
670670
cntl->SetFailed(EREQUEST, "Fail to build redis command");
671671
return false;
672672
}
@@ -1064,7 +1064,7 @@ bool RedisClusterChannel::BuildRedisRequest(const std::vector<std::string>& args
10641064
for (size_t i = 0; i < args.size(); ++i) {
10651065
components.push_back(args[i]);
10661066
}
1067-
return request->AddCommandByComponents(&components[0], components.size());
1067+
return request->AddCommandByComponents(components);
10681068
}
10691069

10701070
void RedisClusterChannel::AppendIntegerReply(butil::IOBuf* buf, int64_t value) {

‎src/brpc/redis_command.cpp‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <cctype>
1919
#include <limits>
20+
#include <vector>
2021

2122
#include "butil/logging.h"
2223
#include "brpc/log.h"
@@ -257,10 +258,27 @@ RedisCommandFormatV(butil::IOBuf* outbuf, const char* fmt, va_list ap) {
257258
if (_l < sizeof(_format)-2) {
258259
memcpy(_format, c, _l);
259260
_format[_l] = '\0';
260-
int plen = vsnprintf(_printed, sizeof(_printed), _format, _cpy);
261-
if (plen > 0) {
261+
va_list _retry;
262+
va_copy(_retry, _cpy);
263+
const int plen = vsnprintf(_printed, sizeof(_printed), _format, _cpy);
264+
if (plen < 0) {
265+
va_end(_retry);
266+
va_end(_cpy);
267+
return butil::Status(EINVAL, "Failed to format redis command");
268+
}
269+
if (static_cast<size_t>(plen) >= sizeof(_printed)) {
270+
std::vector<char> full(static_cast<size_t>(plen) + 1);
271+
const int written = vsnprintf(full.data(), full.size(), _format, _retry);
272+
if (written != plen) {
273+
va_end(_retry);
274+
va_end(_cpy);
275+
return butil::Status(EINVAL, "Failed to format redis command");
276+
}
277+
compbuf.append(full.data(), plen);
278+
} else if (plen > 0) {
262279
compbuf.append(_printed, plen);
263280
}
281+
va_end(_retry);
264282
/* Update current position (note: outer blocks
265283
* increment c twice so compensate here) */
266284
c = _p - 1;

‎test/brpc_redis_unittest.cpp‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,39 @@ class RedisTest : public testing::Test {
139139
void TearDown() {}
140140
};
141141

142+
TEST(RedisCommandFormatTest, wide_numeric_conversion) {
143+
butil::IOBuf buf;
144+
ASSERT_TRUE(brpc::RedisCommandFormat(&buf, "SET key %100d tail %d", 7, 9).ok());
145+
146+
const std::string padded_value(99, ' ');
147+
const std::string expected =
148+
std::string("*5\r\n$3\r\nSET\r\n$3\r\nkey\r\n$100\r\n") +
149+
padded_value + "7\r\n$4\r\ntail\r\n$1\r\n9\r\n";
150+
EXPECT_EQ(expected, buf.to_string());
151+
}
152+
153+
TEST(RedisCommandFormatTest, vector_components_preserve_binary_payload) {
154+
const std::string value("a\0b", 3);
155+
const std::vector<butil::StringPiece> components = {
156+
"SET", "key", butil::StringPiece(value.data(), value.size())};
157+
brpc::RedisRequest request;
158+
ASSERT_TRUE(request.AddCommandByComponents(components));
159+
160+
butil::IOBuf buf;
161+
ASSERT_TRUE(request.SerializeTo(&buf));
162+
std::string expected = "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n";
163+
expected.append(value).append("\r\n");
164+
EXPECT_EQ(expected, buf.to_string());
165+
}
166+
167+
TEST(RedisCommandFormatTest, authenticator_encodes_password_and_database) {
168+
brpc::policy::RedisAuthenticator authenticator("a b", 3);
169+
std::string credential;
170+
ASSERT_EQ(0, authenticator.GenerateCredential(&credential));
171+
EXPECT_EQ("*2\r\n$4\r\nAUTH\r\n$3\r\na b\r\n"
172+
"*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n", credential);
173+
}
174+
142175
void AssertReplyEqual(const brpc::RedisReply& reply1,
143176
const brpc::RedisReply& reply2) {
144177
if (&reply1 == &reply2) {

0 commit comments

Comments
 (0)