Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/stl deserialization validation #6

Merged
merged 2 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,17 @@ ctest .

# Requirements
C++11 or higher.


# DISCLAIMER: STL File Format #


The STL file format, while widely used for 3D modeling and printing, was designed to be simple and easy to parse. However, this simplicity comes with some significant limitations:

- Lack of Built-in Validation Mechanisms: The STL format does not include built-in mechanisms such as checksums, hashes, or any form of file validation. This makes it challenging to detect certain types of file corruption, such as a truncated header or malformed data. As a result, errors in file transmission, storage, or manipulation might go undetected.

- Vulnerability to Corruption: Due to the lack of validation features, STL files can be easily corrupted. For example, if the file is truncated or contains invalid data, these issues may not be detected until the file is parsed or processed, potentially leading to crashes or undefined behavior in applications that use the file.

- Potential for Buffer Overflow Attacks: The lack of built-in validation and the absence of bounds checking in the STL format can make it susceptible to buffer overflow attacks. Care should be taken when handling STL files, especially those from untrusted sources, to ensure they are properly validated before being used.

These limitations are inherent to the STL format and should be considered when working with or implementing software that processes STL files. Developers are encouraged to implement additional validation and error-handling mechanisms in their applications to mitigate these risks.
63 changes: 52 additions & 11 deletions modules/core/include/openstl/core/stl.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,19 @@ namespace openstl
void serializeBinaryStl(const Container& triangles, Stream& stream) {
// Write header (80 bytes for comments)
char header[80] = "STL Exported by OpenSTL [https://github.com/Innoptech/OpenSTL]";
stream.write(header, 80);
stream.write(header, sizeof(header));

// Write triangle count (4 bytes)
auto triangleCount = static_cast<uint32_t>(triangles.size());
stream.write((const char*)&triangleCount, sizeof(triangleCount));
stream.write(reinterpret_cast<const char*>(&triangleCount), sizeof(triangleCount));

// Write triangles
for (const auto& tri : triangles)
stream.write((const char*)&tri, sizeof(Triangle));
for (const auto& tri : triangles) {
stream.write(reinterpret_cast<const char*>(&tri), sizeof(Triangle));
}
}


/**
* @brief Serialize a vector of triangles in the specified STL format and write to a stream.
*
Expand Down Expand Up @@ -171,7 +173,6 @@ namespace openstl
triangles.push_back(tri);
}
}

return triangles;
}

Expand All @@ -183,16 +184,56 @@ namespace openstl
* @return A vector of triangles representing the geometry from the binary STL file.
*/
template <typename Stream>
inline std::vector<Triangle> deserializeBinaryStl(Stream& stream)
{
// Read header
stream.ignore(80); // Ignore the header
std::vector<Triangle> deserializeBinaryStl(Stream& stream) {
// Get the current position and determine the file size
auto start_pos = stream.tellg();
stream.seekg(0, std::ios::end);
auto end_pos = stream.tellg();
stream.seekg(start_pos);

// Ensure the file is large enough for the header and triangle count
if (end_pos - start_pos < 84) {
throw std::runtime_error("File is too small to be a valid STL file.");
}

// Explicitly read the header (80 bytes)
char header[80];
stream.read(header, sizeof(header));

if (stream.gcount() != sizeof(header)) {
throw std::runtime_error("Failed to read the full header. Possible corruption or incomplete file.");
}

// Read and validate triangle count (4 bytes)
uint32_t triangle_qty;
stream.read((char*)&triangle_qty, sizeof(triangle_qty));
stream.read(reinterpret_cast<char*>(&triangle_qty), sizeof(triangle_qty));

if (stream.gcount() != sizeof(triangle_qty) || stream.fail() || stream.eof()) {
throw std::runtime_error("Failed to read the triangle count. Possible corruption or incomplete file.");
}

// Validate triangle count
const uint32_t MAX_TRIANGLES = 1000000;
if (triangle_qty > MAX_TRIANGLES) {
throw std::runtime_error("Triangle count exceeds the maximum allowable value.");
}

// Calculate the expected size of the triangle data
std::size_t expected_data_size = sizeof(Triangle) * triangle_qty;

// Ensure the stream has enough data left
if (end_pos - stream.tellg() < static_cast<std::streamoff>(expected_data_size)) {
throw std::runtime_error("Not enough data in stream for the expected triangle count.");
}

// Read triangles
std::vector<Triangle> triangles(triangle_qty);
stream.read((char*)triangles.data(), sizeof(Triangle)*triangle_qty);
stream.read(reinterpret_cast<char*>(triangles.data()), expected_data_size);

if (stream.gcount() != expected_data_size || stream.fail() || stream.eof()) {
throw std::runtime_error("Failed to read the expected number of triangles. Possible corruption or incomplete file.");
}

return triangles;
}

Expand Down
102 changes: 102 additions & 0 deletions tests/core/src/deserialize.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,107 @@ TEST_CASE("Deserialize Binary STL", "[openstl]") {
}
}

TEST_CASE("Binary STL Serialization/Deserialization Security and Integrity Tests",
"[openstl][security][stl][serialization][deserialization][security]")
{
SECTION("Incomplete triangle data - incomplete_triangle_data.stl") {
const std::vector<Triangle> &triangles = testutils::createTestTriangle();
const std::string filename{"incomplete_triangle_data.stl"};
testutils::createIncompleteTriangleData(triangles, filename);

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Test deserialization with corrupted header (invalid characters)") {
const std::vector<Triangle>& triangles = testutils::createTestTriangle();
const std::string filename = "corrupted_header.stl";
testutils::createCorruptedHeaderInvalidChars(triangles, filename); // Generate the file with invalid characters in the header

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());

std::vector<Triangle> deserialized_triangles;
CHECK_NOTHROW(deserialized_triangles = deserializeBinaryStl(file));
REQUIRE(testutils::checkTrianglesEqual(deserialized_triangles, triangles));
}
SECTION("Test deserialization with corrupted header (excess data after header)") {
const std::vector<Triangle> &triangles = testutils::createTestTriangle();
const std::string filename{"excess_data_after_header.stl"};
testutils::createCorruptedHeaderExcessData(triangles,
filename); // Generate the file with excess data after the header

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Test deserialization with excessive triangle count") {
const std::vector<Triangle> &triangles = testutils::createTestTriangle();
const std::string filename{"excessive_triangle_count.stl"};
testutils::createExcessiveTriangleCount(triangles,filename); // Generate the file with an excessive triangle count

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Test deserialization with the maximum number of triangles") {
const uint32_t MAX_TRIANGLES = 1000000;
const std::string filename = "max_triangles.stl";

// Create a file with exactly MAX_TRIANGLES triangles
std::vector<Triangle> triangles(MAX_TRIANGLES);
testutils::createStlWithTriangles(triangles, filename);

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());

// Test that deserialization works correctly for MAX_TRIANGLES
std::vector<Triangle> deserialized_triangles;
CHECK_NOTHROW(deserialized_triangles = deserializeBinaryStl(file));
REQUIRE(deserialized_triangles.size() == MAX_TRIANGLES);
}
SECTION("Test deserialization exceeding the maximum number of triangles") {
const uint32_t EXCEEDING_TRIANGLES = 1'000'001;
const std::string filename = "exceeding_triangles.stl";

// Create a file with more than MAX_TRIANGLES triangles
std::vector<Triangle> triangles(EXCEEDING_TRIANGLES);
testutils::createStlWithTriangles(triangles, filename);

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());

// Test that deserialization throws an exception for exceeding MAX_TRIANGLES
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Test deserialization with an empty file") {
const std::string filename{"empty_triangles.stl"};
testutils::createEmptyStlFile(filename); // Generate an empty file

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Buffer overflow on triangle count - buffer_overflow_triangle_count.stl") {
std::string filename = "buffer_overflow_triangle_count.stl";
testutils::createBufferOverflowOnTriangleCount(filename);

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());
CHECK_THROWS_AS(deserializeBinaryStl(file), std::runtime_error);
}
SECTION("Test deserialization with corrupted header (invalid characters) - corrupted_header_invalid_chars.stl") {
const std::vector<Triangle>& triangles = testutils::createTestTriangle();
const std::string filename = "corrupted_header_invalid_chars.stl";
testutils::createCorruptedHeaderInvalidChars(triangles, filename); // Generate the file with invalid characters in the header

std::ifstream file(filename, std::ios::binary);
REQUIRE(file.is_open());

// Deserialize the STL file, ignoring the header content
auto deserialized_triangles = deserializeBinaryStl(file);

// Check that the deserialized triangles match the expected count and data
testutils::checkTrianglesEqual(deserialized_triangles, triangles);
}
}
27 changes: 4 additions & 23 deletions tests/core/src/serialize.test.cpp
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
#include <catch2/catch_test_macros.hpp>
#include "openstl/tests/testutils.h"
#include "openstl/core/stl.h"

using namespace openstl;


// Custom equality operator for Vertex struct
bool operator!=(const Vec3& rhs, const Vec3& lhs) {
return std::tie(rhs.x, rhs.y, rhs.z) != std::tie(lhs.x, lhs.y, lhs.z);
}

// Utility function to compare two vectors of triangles
bool compareTriangles(const std::vector<Triangle>& a, const std::vector<Triangle>& b, bool omit_attribute=false) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].normal != b[i].normal ||
a[i].v0 != b[i].v0 ||
a[i].v1 != b[i].v1 ||
a[i].v2 != b[i].v2 ||
((a[i].attribute_byte_count != b[i].attribute_byte_count) & !omit_attribute)) {
return false;
}
}
return true;
}

TEST_CASE("Serialize STL triangles", "[openstl]") {
// Generate some sample triangles
std::vector<Triangle> originalTriangles{
Expand Down Expand Up @@ -51,7 +32,7 @@ TEST_CASE("Serialize STL triangles", "[openstl]") {
auto deserializedTriangles = deserializeBinaryStl(inFile);

// Validate deserialized triangles against original triangles
REQUIRE(compareTriangles(deserializedTriangles, originalTriangles));
REQUIRE(testutils::checkTrianglesEqual(deserializedTriangles, originalTriangles));
}

SECTION("Binary Format stringstream") {
Expand All @@ -65,7 +46,7 @@ TEST_CASE("Serialize STL triangles", "[openstl]") {
auto deserializedTriangles = deserializeBinaryStl(ss);

// Validate deserialized triangles against original triangles
REQUIRE(compareTriangles(deserializedTriangles, originalTriangles));
REQUIRE(testutils::checkTrianglesEqual(deserializedTriangles, originalTriangles));
}

SECTION("ASCII Format") {
Expand All @@ -88,6 +69,6 @@ TEST_CASE("Serialize STL triangles", "[openstl]") {
auto deserializedTriangles = deserializeAsciiStl(inFile);

// Validate deserialized triangles against original triangles
REQUIRE(compareTriangles(deserializedTriangles, originalTriangles, true));
REQUIRE(testutils::checkTrianglesEqual(deserializedTriangles, originalTriangles, true));
}
}
Loading
Loading