Bugfix/validate-user-provided-file-object-paths (#1197)
* Fixed BACnet file object path name unintended path traversals by optionally restricting path name content with BACNET_FILE_PATH_RESTRICTED define. * Added POSIX file path name checking for AtomicReadFile and AtomicWriteFile example applications. Prohibits use of relative and absolute file paths when BACNET_FILE_PATH_RESTRICTED is non-zero.
This commit is contained in:
+7
-1
@@ -12,7 +12,7 @@ The git repositories are hosted at the following sites:
|
|||||||
* https://bacnet.sourceforge.net/
|
* https://bacnet.sourceforge.net/
|
||||||
* https://github.com/bacnet-stack/bacnet-stack/
|
* https://github.com/bacnet-stack/bacnet-stack/
|
||||||
|
|
||||||
## [Unreleased] - 2026-01-03
|
## [Unreleased] - 2026-01-05
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
@@ -26,9 +26,15 @@ The git repositories are hosted at the following sites:
|
|||||||
Fixed ubasic string variables to initialize with zeros.
|
Fixed ubasic string variables to initialize with zeros.
|
||||||
Fixed compile errors when UBASIC_DEBUG_STRINGVARIABLES is defined.
|
Fixed compile errors when UBASIC_DEBUG_STRINGVARIABLES is defined.
|
||||||
Added ubasic string variables user accessor API and unit testing. (#1196)
|
Added ubasic string variables user accessor API and unit testing. (#1196)
|
||||||
|
* Secured BACnet file object pathname received from BACnet AtomicWriteFile
|
||||||
|
or ReadFile service used without validation which was vulnerable to
|
||||||
|
directory traversal attacks. (#1197)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Added file path name checking for AtomicReadFile and AtomicWriteFile
|
||||||
|
example applications. Prohibits use of relative and absolute file paths
|
||||||
|
when BACNET_FILE_PATH_RESTRICTED is defined non-zero. (#1197)
|
||||||
* Added API and optional properties to basic load control object example
|
* Added API and optional properties to basic load control object example
|
||||||
Refactored BACnetShedLevel encoding, decoding, and printing into separate
|
Refactored BACnetShedLevel encoding, decoding, and printing into separate
|
||||||
file. Added BACnetShedLevel validation testing. (#1187)
|
file. Added BACnetShedLevel validation testing. (#1187)
|
||||||
|
|||||||
@@ -286,6 +286,10 @@ int main(int argc, char *argv[])
|
|||||||
/* decode the command line parameters */
|
/* decode the command line parameters */
|
||||||
Target_Device_Object_Instance = strtol(argv[1], NULL, 0);
|
Target_Device_Object_Instance = strtol(argv[1], NULL, 0);
|
||||||
Target_File_Object_Instance = strtol(argv[2], NULL, 0);
|
Target_File_Object_Instance = strtol(argv[2], NULL, 0);
|
||||||
|
if (!filename_path_valid(argv[3])) {
|
||||||
|
fprintf(stderr, "Invalid file path: %s\n", argv[3]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Local_File_Name = argv[3];
|
Local_File_Name = argv[3];
|
||||||
if (Target_Device_Object_Instance > BACNET_MAX_INSTANCE) {
|
if (Target_Device_Object_Instance > BACNET_MAX_INSTANCE) {
|
||||||
fprintf(
|
fprintf(
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ int main(int argc, char *argv[])
|
|||||||
/* decode the command line parameters */
|
/* decode the command line parameters */
|
||||||
Target_Device_Object_Instance = strtol(argv[1], NULL, 0);
|
Target_Device_Object_Instance = strtol(argv[1], NULL, 0);
|
||||||
Target_File_Object_Instance = strtol(argv[2], NULL, 0);
|
Target_File_Object_Instance = strtol(argv[2], NULL, 0);
|
||||||
|
if (!filename_path_valid(argv[3])) {
|
||||||
|
fprintf(stderr, "Invalid file path: %s\n", argv[3]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Local_File_Name = argv[3];
|
Local_File_Name = argv[3];
|
||||||
if (Target_Device_Object_Instance > BACNET_MAX_INSTANCE) {
|
if (Target_Device_Object_Instance > BACNET_MAX_INSTANCE) {
|
||||||
fprintf(
|
fprintf(
|
||||||
|
|||||||
+13
-10
@@ -13,11 +13,14 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
/* BACnet Stack defines - first */
|
/* BACnet Stack defines - first */
|
||||||
#include "bacnet/bacdef.h"
|
#include "bacnet/bacdef.h"
|
||||||
#include "bacnet/basic/sys/debug.h"
|
|
||||||
#include "bacnet/basic/object/bacfile.h"
|
#include "bacnet/basic/object/bacfile.h"
|
||||||
|
#include "bacnet/basic/sys/debug.h"
|
||||||
|
#include "bacnet/basic/sys/filename.h"
|
||||||
|
/* me! */
|
||||||
|
#include "bacfile-posix.h"
|
||||||
|
|
||||||
#ifndef FILE_RECORD_SIZE
|
#ifndef BACNET_FILE_POSIX_RECORD_SIZE
|
||||||
#define FILE_RECORD_SIZE MAX_OCTET_STRING_BYTES
|
#define BACNET_FILE_POSIX_RECORD_SIZE MAX_OCTET_STRING_BYTES
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +53,7 @@ size_t bacfile_posix_file_size(const char *pathname)
|
|||||||
long file_position = 0;
|
long file_position = 0;
|
||||||
size_t file_size = 0;
|
size_t file_size = 0;
|
||||||
|
|
||||||
if (pathname) {
|
if (filename_path_valid(pathname)) {
|
||||||
pFile = fopen(pathname, "rb");
|
pFile = fopen(pathname, "rb");
|
||||||
if (pFile) {
|
if (pFile) {
|
||||||
file_position = fsize(pFile);
|
file_position = fsize(pFile);
|
||||||
@@ -100,7 +103,7 @@ size_t bacfile_posix_read_stream_data(
|
|||||||
FILE *pFile = NULL;
|
FILE *pFile = NULL;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
|
|
||||||
if (pathname) {
|
if (filename_path_valid(pathname)) {
|
||||||
pFile = fopen(pathname, "rb");
|
pFile = fopen(pathname, "rb");
|
||||||
if (pFile) {
|
if (pFile) {
|
||||||
(void)fseek(pFile, fileStartPosition, SEEK_SET);
|
(void)fseek(pFile, fileStartPosition, SEEK_SET);
|
||||||
@@ -131,7 +134,7 @@ size_t bacfile_posix_write_stream_data(
|
|||||||
size_t bytes_written = 0;
|
size_t bytes_written = 0;
|
||||||
FILE *pFile = NULL;
|
FILE *pFile = NULL;
|
||||||
|
|
||||||
if (pathname) {
|
if (filename_path_valid(pathname)) {
|
||||||
if (fileStartPosition == 0) {
|
if (fileStartPosition == 0) {
|
||||||
/* open the file as a clean slate when starting at 0 */
|
/* open the file as a clean slate when starting at 0 */
|
||||||
pFile = fopen(pathname, "wb");
|
pFile = fopen(pathname, "wb");
|
||||||
@@ -177,11 +180,11 @@ bool bacfile_posix_write_record_data(
|
|||||||
bool status = false;
|
bool status = false;
|
||||||
FILE *pFile = NULL;
|
FILE *pFile = NULL;
|
||||||
uint32_t i = 0;
|
uint32_t i = 0;
|
||||||
char dummy_data[FILE_RECORD_SIZE];
|
char dummy_data[BACNET_FILE_POSIX_RECORD_SIZE];
|
||||||
const char *pData = NULL;
|
const char *pData = NULL;
|
||||||
size_t fileSeekRecord = 0;
|
size_t fileSeekRecord = 0;
|
||||||
|
|
||||||
if (pathname) {
|
if (filename_path_valid(pathname)) {
|
||||||
if (fileStartRecord == 0) {
|
if (fileStartRecord == 0) {
|
||||||
/* open the file as a clean slate when starting at 0 */
|
/* open the file as a clean slate when starting at 0 */
|
||||||
pFile = fopen(pathname, "wb");
|
pFile = fopen(pathname, "wb");
|
||||||
@@ -238,11 +241,11 @@ bool bacfile_posix_read_record_data(
|
|||||||
bool status = false;
|
bool status = false;
|
||||||
FILE *pFile = NULL;
|
FILE *pFile = NULL;
|
||||||
uint32_t i = 0;
|
uint32_t i = 0;
|
||||||
char dummy_data[FILE_RECORD_SIZE] = { 0 };
|
char dummy_data[BACNET_FILE_POSIX_RECORD_SIZE] = { 0 };
|
||||||
const char *pData = NULL;
|
const char *pData = NULL;
|
||||||
size_t fileSeekRecord = 0;
|
size_t fileSeekRecord = 0;
|
||||||
|
|
||||||
if (pathname) {
|
if (filename_path_valid(pathname)) {
|
||||||
pFile = fopen(pathname, "rb");
|
pFile = fopen(pathname, "rb");
|
||||||
if (pFile) {
|
if (pFile) {
|
||||||
fileSeekRecord = fileStartRecord + fileIndexRecord;
|
fileSeekRecord = fileStartRecord + fileIndexRecord;
|
||||||
|
|||||||
@@ -29,9 +29,6 @@
|
|||||||
#include "bacnet/basic/sys/keylist.h"
|
#include "bacnet/basic/sys/keylist.h"
|
||||||
#include "bacnet/basic/tsm/tsm.h"
|
#include "bacnet/basic/tsm/tsm.h"
|
||||||
|
|
||||||
#ifndef FILE_RECORD_SIZE
|
|
||||||
#define FILE_RECORD_SIZE MAX_OCTET_STRING_BYTES
|
|
||||||
#endif
|
|
||||||
struct object_data {
|
struct object_data {
|
||||||
char *Object_Name;
|
char *Object_Name;
|
||||||
char *Pathname;
|
char *Pathname;
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* @file
|
* @file
|
||||||
* @brief Function for filename manipulation
|
* @brief Function for filename and path manipulation and validation
|
||||||
* @author Steve Karg <skarg@users.sourceforge.net>
|
* @author Steve Karg <skarg@users.sourceforge.net>
|
||||||
* @date 2007
|
* @date 2007
|
||||||
* @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
|
* @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
|
||||||
*/
|
*/
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include "bacnet/basic/sys/debug.h"
|
||||||
#include "bacnet/basic/sys/filename.h"
|
#include "bacnet/basic/sys/filename.h"
|
||||||
|
|
||||||
|
/* restrict file paths */
|
||||||
|
#ifndef BACNET_FILE_PATH_RESTRICTED
|
||||||
|
#define BACNET_FILE_PATH_RESTRICTED 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove path from filename
|
||||||
|
* @param filename_in - input filename with path
|
||||||
|
* @return filename without path
|
||||||
|
*/
|
||||||
const char *filename_remove_path(const char *filename_in)
|
const char *filename_remove_path(const char *filename_in)
|
||||||
{
|
{
|
||||||
const char *filename_out = filename_in;
|
const char *filename_out = filename_in;
|
||||||
@@ -30,3 +41,57 @@ const char *filename_remove_path(const char *filename_in)
|
|||||||
|
|
||||||
return filename_out;
|
return filename_out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Validate if pathname is valid by checking for patterns
|
||||||
|
* such as relative paths and absolute paths which are prohibited.
|
||||||
|
* @param pathname Path to validate
|
||||||
|
* @return true if valid, false if not
|
||||||
|
*/
|
||||||
|
bool filename_path_valid(const char *pathname)
|
||||||
|
{
|
||||||
|
int path_len;
|
||||||
|
|
||||||
|
if (!pathname) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pathname[0] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#if BACNET_FILE_PATH_RESTRICTED
|
||||||
|
/* check for relative directory patterns */
|
||||||
|
if (strstr(pathname, "..") != NULL) {
|
||||||
|
debug_printf_stderr("Relative paths are prohibited: %s\n", pathname);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* check for absolute paths */
|
||||||
|
if (pathname[0] == '/') {
|
||||||
|
debug_printf_stderr("Absolute paths are prohibited: %s\n", pathname);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* check for Windows drive letters (should be relative paths only) */
|
||||||
|
path_len = strlen(pathname);
|
||||||
|
if (path_len >= 2 && pathname[1] == ':') {
|
||||||
|
debug_printf_stderr(
|
||||||
|
"Windows drive letters are prohibited: %s\n", pathname);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check for consecutive path separators */
|
||||||
|
if (strstr(pathname, "//") != NULL || strstr(pathname, "\\\\") != NULL) {
|
||||||
|
debug_printf_stderr(
|
||||||
|
"Consecutive path separators are prohibited: %s\n", pathname);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check for path components that are just dots */
|
||||||
|
if (strstr(pathname, "/./") != NULL || strstr(pathname, "\\./") != NULL ||
|
||||||
|
strstr(pathname, "/.\\") != NULL || strstr(pathname, "\\.\\") != NULL) {
|
||||||
|
debug_printf_stderr(
|
||||||
|
"Current directory references are prohibited: %s\n", pathname);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
#define BACNET_SYS_FILENAME_H
|
#define BACNET_SYS_FILENAME_H
|
||||||
/* BACnet Stack defines - first */
|
/* BACnet Stack defines - first */
|
||||||
#include "bacnet/bacdef.h"
|
#include "bacnet/bacdef.h"
|
||||||
|
/* standard includes */
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -16,6 +20,8 @@ extern "C" {
|
|||||||
|
|
||||||
BACNET_STACK_EXPORT
|
BACNET_STACK_EXPORT
|
||||||
const char *filename_remove_path(const char *filename_in);
|
const char *filename_remove_path(const char *filename_in);
|
||||||
|
BACNET_STACK_EXPORT
|
||||||
|
bool filename_path_valid(const char *pathname);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ add_executable(${PROJECT_NAME}
|
|||||||
${SRC_DIR}/bacnet/basic/sys/debug.c
|
${SRC_DIR}/bacnet/basic/sys/debug.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/bigend.c
|
${SRC_DIR}/bacnet/basic/sys/bigend.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/days.c
|
${SRC_DIR}/bacnet/basic/sys/days.c
|
||||||
|
${SRC_DIR}/bacnet/basic/sys/filename.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/keylist.c
|
${SRC_DIR}/bacnet/basic/sys/keylist.c
|
||||||
${SRC_DIR}/bacnet/datalink/bvlc.c
|
${SRC_DIR}/bacnet/datalink/bvlc.c
|
||||||
${SRC_DIR}/bacnet/datalink/bvlc6.c
|
${SRC_DIR}/bacnet/datalink/bvlc6.c
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ add_executable(${PROJECT_NAME}
|
|||||||
# File(s) under test
|
# File(s) under test
|
||||||
${SRC_DIR}/bacnet/basic/sys/filename.c
|
${SRC_DIR}/bacnet/basic/sys/filename.c
|
||||||
# Support files and stubs (pathname alphabetical)
|
# Support files and stubs (pathname alphabetical)
|
||||||
|
${SRC_DIR}/bacnet/basic/sys/debug.c
|
||||||
# Test and test library files
|
# Test and test library files
|
||||||
./src/main.c
|
./src/main.c
|
||||||
${ZTST_DIR}/ztest_mock.c
|
${ZTST_DIR}/ztest_mock.c
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ static void testFilename(void)
|
|||||||
const char *data3 = "c:\\Program Files\\Christopher\\run.exe";
|
const char *data3 = "c:\\Program Files\\Christopher\\run.exe";
|
||||||
const char *data4 = "//Mary/data/run";
|
const char *data4 = "//Mary/data/run";
|
||||||
const char *data5 = "bin\\run";
|
const char *data5 = "bin\\run";
|
||||||
|
const char *data6 = "run.exe";
|
||||||
const char *filename = NULL;
|
const char *filename = NULL;
|
||||||
|
|
||||||
filename = filename_remove_path(data1);
|
filename = filename_remove_path(data1);
|
||||||
@@ -39,9 +40,53 @@ static void testFilename(void)
|
|||||||
zassert_equal(strcmp("run", filename), 0, NULL);
|
zassert_equal(strcmp("run", filename), 0, NULL);
|
||||||
filename = filename_remove_path(data5);
|
filename = filename_remove_path(data5);
|
||||||
zassert_equal(strcmp("run", filename), 0, NULL);
|
zassert_equal(strcmp("run", filename), 0, NULL);
|
||||||
|
filename = filename_remove_path(data6);
|
||||||
|
zassert_equal(strcmp("run.exe", filename), 0, NULL);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(CONFIG_ZTEST_NEW_API)
|
||||||
|
ZTEST(filename_tests, testFilenameValid)
|
||||||
|
#else
|
||||||
|
static void testFilenameValid(void)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
const char *data0 = "";
|
||||||
|
const char *data1 = "c:\\Joshua\\run";
|
||||||
|
const char *data2 = "/home/Anna/run";
|
||||||
|
const char *data3 = "c:\\Program Files\\Christopher\\run.exe";
|
||||||
|
const char *data4 = "//Mary/data/run";
|
||||||
|
const char *data5 = "bin\\\\run";
|
||||||
|
const char *data6 = "bin/./run";
|
||||||
|
const char *data7 = "bin/../run";
|
||||||
|
const char *data_valid = "certs/mycert.pem";
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
valid = filename_path_valid(NULL);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data0);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data1);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data2);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data3);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data4);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data5);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data6);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data7);
|
||||||
|
zassert_false(valid, NULL);
|
||||||
|
valid = filename_path_valid(data_valid);
|
||||||
|
zassert_true(valid, NULL);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +96,9 @@ ZTEST_SUITE(filename_tests, NULL, NULL, NULL, NULL, NULL);
|
|||||||
#else
|
#else
|
||||||
void test_main(void)
|
void test_main(void)
|
||||||
{
|
{
|
||||||
ztest_test_suite(filename_tests, ztest_unit_test(testFilename));
|
ztest_test_suite(
|
||||||
|
filename_tests, ztest_unit_test(testFilename),
|
||||||
|
ztest_unit_test(testFilenameValid));
|
||||||
|
|
||||||
ztest_run_test_suite(filename_tests);
|
ztest_run_test_suite(filename_tests);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ target_sources(${PROJECT_NAME} PRIVATE
|
|||||||
${SRC_DIR}/bacnet/basic/sys/days.c
|
${SRC_DIR}/bacnet/basic/sys/days.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/debug.c
|
${SRC_DIR}/bacnet/basic/sys/debug.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/fifo.c
|
${SRC_DIR}/bacnet/basic/sys/fifo.c
|
||||||
|
${SRC_DIR}/bacnet/basic/sys/filename.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/keylist.c
|
${SRC_DIR}/bacnet/basic/sys/keylist.c
|
||||||
${SRC_DIR}/bacnet/basic/sys/mstimer.c
|
${SRC_DIR}/bacnet/basic/sys/mstimer.c
|
||||||
${SRC_DIR}/bacnet/access_rule.c
|
${SRC_DIR}/bacnet/access_rule.c
|
||||||
|
|||||||
Reference in New Issue
Block a user