test: Add 4 and update 6 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 1m23s
Build-Test-Publish / build (linux/arm64) (push) Successful in 2m21s
Build-Test-Publish / create-manifest (push) Successful in 12s

This commit is contained in:
Your Name
2025-08-10 23:31:41 +12:00
parent 22d4af7ac8
commit baa215e762
10 changed files with 978 additions and 23 deletions

View File

@@ -3,6 +3,7 @@
#include "compress.hpp"
#include "string_utils.hpp"
#include "utils.hpp"
#include "validation.hpp"
#include <drogon/MultiPart.h>
#include <random>
@@ -126,6 +127,18 @@ void PutHandler::handle_upload_object(const drogon::HttpRequestPtr& req, std::fu
}
}
// Validate metadata using comprehensive validation
auto metadataValidation = InputValidator::validateMetadata(metadata);
if (!metadataValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid metadata: " + metadataValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
// Validate required metadata fields
if (!metadata.contains("labeltags") || !metadata["labeltags"].is_array() || metadata["labeltags"].empty()) {
resp->setStatusCode(drogon::k400BadRequest);
@@ -137,30 +150,65 @@ void PutHandler::handle_upload_object(const drogon::HttpRequestPtr& req, std::fu
return;
}
// Validate each label:tag pair format
for (const auto& labeltag : metadata["labeltags"]) {
if (!labeltag.is_string()) {
// Extract and validate labeltags
std::vector<std::string> labeltags;
for (const auto& lt : metadata["labeltags"]) {
if (!lt.is_string()) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid label:tag pair format - must be a string"}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
std::string pair = labeltag.get<std::string>();
if (pair.find(':') == std::string::npos) {
labeltags.push_back(lt.get<std::string>());
}
// Validate all labeltags using input validator
auto labeltagsValidation = InputValidator::validateLabelTags(labeltags);
if (!labeltagsValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", labeltagsValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
// Validate and add filename to metadata if not provided
std::string filename = fileData->getFileName();
if (!filename.empty()) {
auto filenameValidation = InputValidator::validateFilename(filename);
if (!filenameValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid label:tag pair format - must contain ':' separator"}};
nlohmann::json response = {{"result", "error"}, {"error", "Invalid filename: " + filenameValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
}
// Add filename to metadata if not provided
if (!metadata.contains("filename")) {
metadata["filename"] = fileData->getFileName();
// Validate filename in metadata if present (updated)
if (metadata.contains("filename")) {
if (metadata["filename"].is_string()) {
auto filenameInMetadata = metadata["filename"].get<std::string>();
auto filenameValidation = InputValidator::validateFilename(filenameInMetadata);
if (!filenameValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid filename in metadata: " + filenameValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
}
} else if (!filename.empty()) {
metadata["filename"] = filename;
}
// Now that all parameters are validated, process the upload

View File

@@ -18,6 +18,7 @@
#include "utils.hpp"
#include "welcome_page.hpp"
#include "rate_limiter.hpp"
#include "validation.hpp"
#include "HttpController.hpp"
#include "bcrypt.hpp" // For secure token hashing
@@ -415,6 +416,18 @@ void Server::handle_delete_object(const drogon::HttpRequestPtr& req, std::functi
return;
}
// Validate hash format
auto hashValidation = InputValidator::validateHash(params["hash"]);
if (!hashValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid hash: " + hashValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
add_security_headers(resp);
callback(resp);
return;
}
if (!db_->get(params["hash"], entry)) {
resp->setStatusCode(drogon::k404NotFound);
nlohmann::json response = {{"result", "error"}, {"error", "Object not found for: " + params["hash"]}};

View File

@@ -1,4 +1,5 @@
#include "update_handler.hpp"
#include "validation.hpp"
#include <nlohmann/json.hpp>
#include <drogon/MultiPart.h>
#include <iostream>
@@ -114,6 +115,60 @@ void UpdateHandler::handle_update_object(const drogon::HttpRequestPtr& req, std:
std::string hash = body["hash"].get<std::string>();
nlohmann::json new_metadata = body["metadata"];
// Validate hash format
auto hashValidation = InputValidator::validateHash(hash);
if (!hashValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid hash: " + hashValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
// Validate metadata
auto metadataValidation = InputValidator::validateMetadata(new_metadata);
if (!metadataValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid metadata: " + metadataValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
// If metadata contains labeltags, validate them
if (new_metadata.contains("labeltags") && new_metadata["labeltags"].is_array()) {
std::vector<std::string> labeltags;
for (const auto& lt : new_metadata["labeltags"]) {
if (!lt.is_string()) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid label:tag format in metadata"}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
labeltags.push_back(lt.get<std::string>());
}
if (!labeltags.empty()) {
auto labeltagsValidation = InputValidator::validateLabelTags(labeltags);
if (!labeltagsValidation.valid) {
resp->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", labeltagsValidation.error}};
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
server_.add_security_headers(resp);
callback(resp);
return;
}
}
}
// Get the object entry
dbEntry entry;
if (!server_.db_->get(hash, entry)) {

303
src/validation.cpp Normal file
View File

@@ -0,0 +1,303 @@
#include "validation.hpp"
#include <algorithm>
#include <cctype>
namespace simple_object_storage {
// Define regex patterns
// Labels and tags: alphanumeric, dash, underscore, dot
// Label:tag format: label:tag where both parts follow the pattern
const std::regex InputValidator::LABEL_TAG_PATTERN("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}:[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$");
const std::regex InputValidator::LABEL_PATTERN("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$");
const std::regex InputValidator::TAG_PATTERN("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$");
const std::regex InputValidator::HASH_PATTERN("^[a-f0-9]{64}$"); // SHA-256 lowercase hex
const std::regex InputValidator::JSON_FIELD_PATTERN("^[a-zA-Z_][a-zA-Z0-9_]{0,127}$");
InputValidator::ValidationResult InputValidator::validateLabelTag(const std::string& labeltag) {
// Check for empty string
if (labeltag.empty()) {
return ValidationResult(false, "Label:tag cannot be empty");
}
// Check maximum length
if (labeltag.length() > MAX_LABELTAG_LENGTH) {
return ValidationResult(false, "Label:tag exceeds maximum length of " +
std::to_string(MAX_LABELTAG_LENGTH) + " characters");
}
// Check for colon separator
size_t colonPos = labeltag.find(':');
if (colonPos == std::string::npos) {
return ValidationResult(false, "Label:tag must contain a colon separator");
}
// Check that there's exactly one colon
if (labeltag.find(':', colonPos + 1) != std::string::npos) {
return ValidationResult(false, "Label:tag must contain exactly one colon separator");
}
// Extract label and tag
std::string label = labeltag.substr(0, colonPos);
std::string tag = labeltag.substr(colonPos + 1);
// Validate label
auto labelResult = validateLabel(label);
if (!labelResult.valid) {
return ValidationResult(false, "Invalid label: " + labelResult.error);
}
// Validate tag
auto tagResult = validateTag(tag);
if (!tagResult.valid) {
return ValidationResult(false, "Invalid tag: " + tagResult.error);
}
// Check against regex pattern for additional validation
if (!std::regex_match(labeltag, LABEL_TAG_PATTERN)) {
return ValidationResult(false, "Label:tag contains invalid characters or format");
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateLabel(const std::string& label) {
if (label.empty()) {
return ValidationResult(false, "Label cannot be empty");
}
if (label.length() > MAX_LABEL_LENGTH) {
return ValidationResult(false, "Label exceeds maximum length of " +
std::to_string(MAX_LABEL_LENGTH) + " characters");
}
// Must start with alphanumeric
if (!std::isalnum(static_cast<unsigned char>(label[0]))) {
return ValidationResult(false, "Label must start with an alphanumeric character");
}
// Check against pattern
if (!std::regex_match(label, LABEL_PATTERN)) {
return ValidationResult(false, "Label contains invalid characters. Only alphanumeric, dash, underscore, and dot allowed");
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateTag(const std::string& tag) {
if (tag.empty()) {
return ValidationResult(false, "Tag cannot be empty");
}
if (tag.length() > MAX_TAG_LENGTH) {
return ValidationResult(false, "Tag exceeds maximum length of " +
std::to_string(MAX_TAG_LENGTH) + " characters");
}
// Must start with alphanumeric
if (!std::isalnum(static_cast<unsigned char>(tag[0]))) {
return ValidationResult(false, "Tag must start with an alphanumeric character");
}
// Check against pattern
if (!std::regex_match(tag, TAG_PATTERN)) {
return ValidationResult(false, "Tag contains invalid characters. Only alphanumeric, dash, underscore, and dot allowed");
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateLabelTags(const std::vector<std::string>& labeltags) {
if (labeltags.empty()) {
return ValidationResult(false, "At least one label:tag is required");
}
if (labeltags.size() > MAX_LABELTAGS_COUNT) {
return ValidationResult(false, "Too many label:tags. Maximum allowed is " +
std::to_string(MAX_LABELTAGS_COUNT));
}
// Check for duplicates
std::vector<std::string> sorted = labeltags;
std::sort(sorted.begin(), sorted.end());
if (std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end()) {
return ValidationResult(false, "Duplicate label:tag entries found");
}
// Validate each label:tag
for (size_t i = 0; i < labeltags.size(); ++i) {
auto result = validateLabelTag(labeltags[i]);
if (!result.valid) {
return ValidationResult(false, "Invalid label:tag at index " +
std::to_string(i) + ": " + result.error);
}
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateHash(const std::string& hash) {
if (hash.empty()) {
return ValidationResult(false, "Hash cannot be empty");
}
if (hash.length() != MAX_HASH_LENGTH) {
return ValidationResult(false, "Hash must be exactly " +
std::to_string(MAX_HASH_LENGTH) + " characters (SHA-256)");
}
// Convert to lowercase for comparison
std::string lowerHash = hash;
std::transform(lowerHash.begin(), lowerHash.end(), lowerHash.begin(), ::tolower);
if (!std::regex_match(lowerHash, HASH_PATTERN)) {
return ValidationResult(false, "Hash must be a valid SHA-256 hex string");
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateMetadata(const nlohmann::json& metadata) {
// Check if metadata is an object
if (!metadata.is_object()) {
return ValidationResult(false, "Metadata must be a JSON object");
}
// Check size
std::string metadataStr = metadata.dump();
if (metadataStr.length() > MAX_METADATA_SIZE) {
return ValidationResult(false, "Metadata exceeds maximum size of " +
std::to_string(MAX_METADATA_SIZE) + " bytes");
}
// Validate each field
for (auto& [key, value] : metadata.items()) {
// Validate field name
if (!isValidJsonFieldName(key)) {
return ValidationResult(false, "Invalid metadata field name: " + key +
". Field names must start with a letter or underscore and contain only alphanumeric characters and underscores");
}
// Check field name length
if (key.length() > 128) {
return ValidationResult(false, "Metadata field name too long: " + key);
}
// Check value type and size
if (value.is_string()) {
if (value.get<std::string>().length() > MAX_JSON_FIELD_LENGTH) {
return ValidationResult(false, "Metadata field '" + key +
"' value exceeds maximum length of " +
std::to_string(MAX_JSON_FIELD_LENGTH));
}
} else if (value.is_array()) {
// Limit array size
if (value.size() > 1000) {
return ValidationResult(false, "Metadata field '" + key +
"' array exceeds maximum size of 1000 elements");
}
// Check each array element if it's a string
for (const auto& elem : value) {
if (elem.is_string() && elem.get<std::string>().length() > MAX_JSON_FIELD_LENGTH) {
return ValidationResult(false, "Metadata field '" + key +
"' array element exceeds maximum string length");
}
}
} else if (value.is_object()) {
// Recursively validate nested objects (with depth limit)
static int depth = 0;
if (++depth > 5) {
--depth;
return ValidationResult(false, "Metadata nesting depth exceeds maximum of 5 levels");
}
auto result = validateMetadata(value);
--depth;
if (!result.valid) {
return ValidationResult(false, "Invalid nested metadata in field '" + key + "': " + result.error);
}
}
}
return ValidationResult(true);
}
InputValidator::ValidationResult InputValidator::validateFilename(const std::string& filename) {
if (filename.empty()) {
return ValidationResult(false, "Filename cannot be empty");
}
if (filename.length() > MAX_FILENAME_LENGTH) {
return ValidationResult(false, "Filename exceeds maximum length of " +
std::to_string(MAX_FILENAME_LENGTH) + " characters");
}
// Check for directory traversal attempts
if (filename.find("..") != std::string::npos) {
return ValidationResult(false, "Filename cannot contain '..' (directory traversal)");
}
if (filename.find("/") != std::string::npos || filename.find("\\") != std::string::npos) {
return ValidationResult(false, "Filename cannot contain path separators");
}
// Check for null bytes
if (filename.find('\0') != std::string::npos) {
return ValidationResult(false, "Filename cannot contain null bytes");
}
// Check for control characters
for (char c : filename) {
if (std::iscntrl(static_cast<unsigned char>(c))) {
return ValidationResult(false, "Filename cannot contain control characters");
}
}
// Disallow certain problematic filenames
std::string lowerFilename = filename;
std::transform(lowerFilename.begin(), lowerFilename.end(), lowerFilename.begin(), ::tolower);
// Windows reserved names
const std::vector<std::string> reserved = {
"con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5",
"com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9"
};
for (const auto& r : reserved) {
if (lowerFilename == r || lowerFilename.find(r + ".") == 0) {
return ValidationResult(false, "Filename uses a reserved system name");
}
}
return ValidationResult(true);
}
bool InputValidator::isValidLabelTagCharset(const std::string& str) {
for (char c : str) {
if (!std::isalnum(static_cast<unsigned char>(c)) &&
c != '-' && c != '_' && c != '.' && c != ':') {
return false;
}
}
return true;
}
bool InputValidator::isValidJsonFieldName(const std::string& fieldName) {
if (fieldName.empty()) return false;
// Must start with letter or underscore
if (!std::isalpha(static_cast<unsigned char>(fieldName[0])) && fieldName[0] != '_') {
return false;
}
// Rest can be alphanumeric or underscore
for (size_t i = 1; i < fieldName.length(); ++i) {
char c = fieldName[i];
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
return false;
}
}
return true;
}
} // namespace simple_object_storage

66
src/validation.hpp Normal file
View File

@@ -0,0 +1,66 @@
#pragma once
#include <string>
#include <vector>
#include <regex>
#include <nlohmann/json.hpp>
namespace simple_object_storage {
class InputValidator {
public:
// Maximum lengths for various inputs
static constexpr size_t MAX_LABEL_LENGTH = 255;
static constexpr size_t MAX_TAG_LENGTH = 255;
static constexpr size_t MAX_LABELTAG_LENGTH = 512; // label:tag combined
static constexpr size_t MAX_METADATA_SIZE = 1024 * 1024; // 1MB for metadata JSON
static constexpr size_t MAX_HASH_LENGTH = 64; // SHA-256 hex string
static constexpr size_t MAX_FILENAME_LENGTH = 255;
static constexpr size_t MAX_JSON_FIELD_LENGTH = 4096;
static constexpr size_t MAX_LABELTAGS_COUNT = 100; // Max number of label:tags per object
// Validation result structure
struct ValidationResult {
bool valid;
std::string error;
ValidationResult(bool v = true, const std::string& e = "") : valid(v), error(e) {}
};
// Validate a single label:tag pair
static ValidationResult validateLabelTag(const std::string& labeltag);
// Validate a label component
static ValidationResult validateLabel(const std::string& label);
// Validate a tag component
static ValidationResult validateTag(const std::string& tag);
// Validate a list of label:tag pairs
static ValidationResult validateLabelTags(const std::vector<std::string>& labeltags);
// Validate a SHA-256 hash
static ValidationResult validateHash(const std::string& hash);
// Validate metadata JSON
static ValidationResult validateMetadata(const nlohmann::json& metadata);
// Validate a filename
static ValidationResult validateFilename(const std::string& filename);
// Check if a string contains only allowed characters for labels/tags
static bool isValidLabelTagCharset(const std::string& str);
// Check if a string is a valid JSON field name
static bool isValidJsonFieldName(const std::string& fieldName);
private:
// Regex patterns for validation
static const std::regex LABEL_TAG_PATTERN;
static const std::regex LABEL_PATTERN;
static const std::regex TAG_PATTERN;
static const std::regex HASH_PATTERN;
static const std::regex JSON_FIELD_PATTERN;
};
} // namespace simple_object_storage