Skip to content

Commit 813bf6f

Browse files
fix: bound HTTP camera response header copies to prevent buffer overflow
RemoteCameraHTTP::GetResponse()'s non-PCRE parser (the default when built without PCRE) copied server-controlled HTTP response header values into fixed-size static buffers with unbounded strcpy()/sprintf(), and with strncpy() bounds derived from delimiters rather than the buffer size. A malicious or MitM'd HTTP camera could overflow status_mesg[256], connection_type[32], content_type[32] and content_boundary[64], corrupting adjacent parser state (content_length, content_boundary_len) for DoS and secondary heap corruption. Add zm_strncpy(): a bounded copy that always null-terminates (strcpy overflows; strncpy skips the terminator on truncation) and optionally caps to a field length n for delimiter-bounded values whose source is not null-terminated at the field end. Route every header copy in GetResponse() through it. The content_boundary "--" prefix is written separately and content_boundary_len is taken from strlen() of the resulting (now bounded) string so the compare length at the multipart subheader match stays correct. The two subcontent-header strncpy() calls were already bounded to sizeof-1 with an explicit terminator and are left as-is. Adds a tests/zm_utils.cpp self-check covering fit, truncation, delimiter-limit, delimiter-over-buffer, and empty-source cases. Refs GHSA-93j4-rcp9-9jx6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b27b67 commit 813bf6f

3 files changed

Lines changed: 51 additions & 13 deletions

File tree

src/zm_remote_camera_http.cpp

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -719,21 +719,18 @@ int RemoteCameraHttp::GetResponse() {
719719
start_ptr = http_header;
720720
end_ptr = start_ptr+strspn(start_ptr, "10.");
721721

722-
// FIXME Why are we memsetting every time? Can we not do it once?
723-
//memset(http_version, 0, sizeof(http_version));
724-
strncpy(http_version, start_ptr, end_ptr-start_ptr);
722+
zm_strncpy(http_version, start_ptr, sizeof(http_version), end_ptr-start_ptr);
725723

726724
start_ptr = end_ptr;
727725
start_ptr += strspn(start_ptr, " ");
728726
end_ptr = start_ptr+strspn(start_ptr, "0123456789");
729727

730-
memset(status_code, 0, sizeof(status_code));
731-
strncpy(status_code, start_ptr, end_ptr-start_ptr);
728+
zm_strncpy(status_code, start_ptr, sizeof(status_code), end_ptr-start_ptr);
732729
int status = atoi(status_code);
733730

734731
start_ptr = end_ptr;
735732
start_ptr += strspn(start_ptr, " ");
736-
strcpy(status_mesg, start_ptr);
733+
zm_strncpy(status_mesg, start_ptr, sizeof(status_mesg));
737734

738735
if (status == 401) {
739736
if (mNeedAuth) {
@@ -771,10 +768,8 @@ int RemoteCameraHttp::GetResponse() {
771768
Debug(3, "Got status '%d' (%s), http version %s", status, status_mesg, http_version);
772769

773770
if (connection_header) {
774-
memset(connection_type, 0, sizeof(connection_type));
775771
start_ptr = connection_header + strspn(connection_header, " ");
776-
// FIXME Should we not use strncpy?
777-
strcpy(connection_type, start_ptr);
772+
zm_strncpy(connection_type, start_ptr, sizeof(connection_type));
778773
Debug(3, "Got connection '%s'", connection_type);
779774
}
780775
if (content_length_header) {
@@ -786,21 +781,23 @@ int RemoteCameraHttp::GetResponse() {
786781
//memset(content_type, 0, sizeof(content_type));
787782
start_ptr = content_type_header + strspn(content_type_header, " ");
788783
if ( (end_ptr = strchr(start_ptr, ';')) ) {
789-
strncpy(content_type, start_ptr, end_ptr-start_ptr);
784+
zm_strncpy(content_type, start_ptr, sizeof(content_type), end_ptr-start_ptr);
790785
Debug(3, "Got content type '%s'", content_type);
791786

792787
start_ptr = end_ptr + strspn(end_ptr, "; ");
793788

794789
if (strncasecmp(start_ptr, boundary_match, boundary_match_len) == 0) {
795790
start_ptr += boundary_match_len;
796791
start_ptr += strspn(start_ptr, "-");
797-
content_boundary_len = sprintf(content_boundary, "--%s", start_ptr);
792+
zm_strncpy(content_boundary, "--", sizeof(content_boundary));
793+
zm_strncpy(content_boundary+2, start_ptr, sizeof(content_boundary)-2);
794+
content_boundary_len = strlen(content_boundary);
798795
Debug(3, "Got content boundary '%s'", content_boundary);
799796
} else {
800797
Error("No content boundary found in header '%s'", content_type_header);
801798
}
802799
} else {
803-
strcpy(content_type, start_ptr);
800+
zm_strncpy(content_type, start_ptr, sizeof(content_type));
804801
Debug(3, "Got content type '%s'", content_type);
805802
}
806803
} // end if content_type_header
@@ -940,7 +937,7 @@ int RemoteCameraHttp::GetResponse() {
940937
if (subcontent_type_header[0]) {
941938
//memset(content_type, 0, sizeof(content_type));
942939
start_ptr = subcontent_type_header + strspn(subcontent_type_header, " ");
943-
strcpy(content_type, start_ptr);
940+
zm_strncpy(content_type, start_ptr, sizeof(content_type));
944941
Debug(3, "Got subcontent type '%s'", content_type);
945942
}
946943
state = CONTENT;

src/zm_utils.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <string>
3333
#include <sys/time.h>
3434
#include <vector>
35+
#include <cstring>
3536

3637

3738
#ifdef NDEBUG
@@ -43,6 +44,19 @@
4344

4445
typedef std::vector<std::string> StringVector;
4546

47+
// Bounded string copy into a fixed-size buffer that ALWAYS null-terminates
48+
// (unlike strcpy, which overflows, and strncpy, which skips the terminator on
49+
// truncation). Copies at most n bytes from src when n >= 0 (for
50+
// delimiter-bounded fields where src is not null-terminated at the field end),
51+
// otherwise up to src's null terminator. dst holds `size` bytes.
52+
inline void zm_strncpy(char *dst, const char *src, size_t size, ssize_t n = -1) {
53+
if (!size) return;
54+
size_t len = (n >= 0) ? static_cast<size_t>(n) : strlen(src);
55+
if (len >= size) len = size - 1;
56+
memcpy(dst, src, len);
57+
dst[len] = '\0';
58+
}
59+
4660
std::string escape_json_string(std::string input);
4761
std::string remove_newlines(std::string input);
4862
std::string Trim(const std::string &str, const std::string &char_set);

tests/zm_utils.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,30 @@ TEST_CASE("remove_authentication") {
330330
REQUIRE(result == "http://192.168.1.1");
331331
}
332332
}
333+
334+
TEST_CASE("zm_strncpy") {
335+
char buf[8];
336+
337+
SECTION("fits, null-terminated") {
338+
zm_strncpy(buf, "abc", sizeof(buf));
339+
REQUIRE(std::string(buf) == "abc");
340+
}
341+
SECTION("oversize truncated to size-1 and terminated") {
342+
zm_strncpy(buf, "abcdefghijkl", sizeof(buf));
343+
REQUIRE(std::string(buf) == "abcdefg"); // 7 chars + '\0'
344+
REQUIRE(buf[7] == '\0');
345+
}
346+
SECTION("delimiter length n limits copy") {
347+
zm_strncpy(buf, "abcdef", sizeof(buf), 3); // src not null-terminated at field end
348+
REQUIRE(std::string(buf) == "abc");
349+
}
350+
SECTION("delimiter length also capped to buffer") {
351+
zm_strncpy(buf, "abcdefghij", sizeof(buf), 20);
352+
REQUIRE(std::string(buf) == "abcdefg");
353+
REQUIRE(buf[7] == '\0');
354+
}
355+
SECTION("empty source") {
356+
zm_strncpy(buf, "", sizeof(buf));
357+
REQUIRE(buf[0] == '\0');
358+
}
359+
}

0 commit comments

Comments
 (0)