Security Report: The Two-Phase Security Approach in GRIDNET Core's Decentralized UI Mechanics

GRIDNET Core UI Security Report

In our continuous commitment to ensuring the security and integrity of our systems, we routinely analyze and address vulnerabilities in GRIDNET Core’s architecture. The advent of decentralization has shifted paradigms in many ways, not least in user interface design and functionality. With these innovations come new challenges. This report delves into one of the significant security challenges we identified in the decentralized user interface (UI) mechanisms and the comprehensive solution we’ve devised to address it.

The Two-Phase Security Approach in GRIDNET Core’s Decentralized UI Mechanics

GRIDNET Core’s decentralized user interface (UI) mechanisms have introduced cutting-edge innovations in distributed system interfaces. While decentralization provides many benefits, it also presents unique security challenges, especially when combined with embedded web server mechanics. One such vulnerability that emerged pertains to path traversal attacks on the UI server, a kind of security risk that can allow an attacker to access unauthorized files and directories in the system by manipulating directory paths.

To address this vulnerability and to ensure robust security, we’ve introduced a comprehensive two-phase security approach.

Path Traversal Attacks: The Core Vulnerability

A path traversal attack, often referred to as directory traversal, aims to access files and directories stored outside the web root folder. If a web application allows users to input file or directory names but doesn’t validate or sanitize them properly, attackers can use special character sequences (like “…/” or its encoded form “%2e%2e%2f”) to navigate out of the intended directory and access unauthorized files. In the context of GRIDNET Core, given its decentralized UI mechanics, a successful path traversal could not only expose sensitive data but also potentially manipulate or disrupt the decentralized operations.

Two-Phase Security Approach:

  1. Phase 1: Path Sanitization

    Objective: To remove or neutralize unsafe sequences and characters from the path.

    Mechanism:

    • Check for null byte injections, which can be used to trick the path parser.
    • Convert the entire path to lowercase to ensure uniformity and consistency during checks.
    • Iteratively remove URL-encoded characters until no more such characters are detected.
    • Explicitly check for common path traversal sequences and URL encoded forms of those sequences.
    • Lastly, a whitelist approach is used where only specific, known safe characters are allowed.

    Rationale: Before any operation involving the user-input path, it’s crucial to ensure the path is safe. This first line of defense is essential in preempting most of the common attack vectors related to path traversal.

  2. Phase 2: Directory Containment Validation

    Objective: Confirm that the sanitized path remains within the intended directory.

    Mechanism:

    • Combine the sanitized path with the root directory.
    • Check if the combined path remains inside the root directory by comparing it against the predefined root directory.

    Rationale: Even after meticulous sanitization, there might be edge cases or unforeseen sequences that bypass the initial validation. Hence, this phase acts as a safety net. By ensuring the resultant path points to a location within the predefined root directory, we ensure no external directories or files can be accessed.

Conclusion

Decentralization, while groundbreaking, poses unique challenges. The decentralized UI mechanics of GRIDNET Core, coupled with embedded web-server mechanics, made it essential to fortify the system against path traversal attacks. The two-phase approach provides a robust defense mechanism, ensuring that potential attackers cannot exploit or access unauthorized information.

Interesting Code Snippets

/**
 * @brief Sanitizes a given file path.
 *
 * This function performs several sanitization operations on the input path:
 * 1. Checks and rejects paths containing null byte injections.
 * 2. Converts the path to lowercase for consistent checks.
 * 3. Decodes URL-encoded characters (like %2e, %2f, %5c).
 * 4. Checks and rejects paths with common path traversal sequences and encodings.
 * 5. Checks and rejects Windows-specific path traversal attempts.
 * 6. Checks and rejects any characters not whitelisted.
 *
 * If all checks pass, the original path is updated with the sanitized version.
 *
 * @param originalPath The path to be sanitized. It is updated in-place if sanitization is successful.
 * @return Returns true if the path is successfully sanitized without any suspicious sequences or characters.
 *         Returns false otherwise.
 */
bool CTools::sanitizePath(std::string& originalPath)
{
	// Check for null byte injection before any other manipulations
	if (originalPath.find('\0') != std::string::npos) {
		return false;
	}

	std::string path = originalPath;

	// Convert path to lowercase for consistent checks
	std::transform(path.begin(), path.end(), path.begin(), ::tolower);

	// Remove URL encoded characters in a loop until no more encoded characters are found
	bool foundEncodings;
	uint64_t initialLength = originalPath.size();

	do {
		initialLength = path.size();
		foundEncodings = false;

		path = replace_all(path, "%2e", ".");
		
		if(initialLength!=path.size())
		{
			foundEncodings = true;
		}

		path = replace_all(path, "%2f", "/");

		if (initialLength != path.size())
		{
			foundEncodings = true;
		}

		path = replace_all(path, "%5c", "\\");

		if (initialLength != path.size())
		{
			foundEncodings = true;
		}

		// Add other encodings as needed...

	} while (foundEncodings);


	if (path.find("..") != std::string::npos ||
		path.find(":/") != std::string::npos ||     // Windows drive letter
		path.find("\\") != std::string::npos ||     // Backslashes for Windows paths
		path.find("//") != std::string::npos ||     // Double slashes
		path.find("\\\\") != std::string::npos ||   // Double backslashes
		path.find("%252e") != std::string::npos ||  // Double-encoded .
		path.find("%c0%ae") != std::string::npos || // Overlong-encoded /
		path.find("%c0%2e") != std::string::npos || // Overlong-encoded .
		path.find("./") != std::string::npos ||
		path.find("/.") != std::string::npos) {
		return false;
	}


	// Check for Windows path traversal attempts
	if (path.find("\\..\\") != std::string::npos) {
		return false;
	}

	// Whitelist allowed characters
	for (char c : path) {
		if (!std::isalnum(c) && c != '/' && c != '.' && c != '-' && c != '_') {
			return false;
		}
	}

	// If all checks pass, update the original path
	originalPath = path;

	return true;
}

and the other part which the two phases are actually executed:

  // SECURITY - BEGIN

       // PHASE 1: Path Sanitization
       // Objective: Ensure that the requested resource path does not contain any suspicious sequences or characters.
       // Mechanism: 
       //   - The sanitizePath function checks for null byte injections, common path traversal sequences, 
       //     URL-encoded characters, and other potential threats.
       //   - It also whitelist only specific characters deemed safe, preventing a wide range of potential attacks.
       // If the path fails this initial sanitization check, we deny the request.
       //Phase 1 - BEGIN
         if (!tools->sanitizePath(s))
         {
             mg_http_reply(c, 403, "", "Oh you naughty, naughty Boy! (..)\n", (int)hm->uri.len, hm->uri.ptr);
             return false;
         }
         //Phase 1 - END

         // PHASE 2: Directory Containment Validation
         // Objective: Confirm that the sanitized path is still contained within the allowed directory.
         // Rationale:
         //   - Even after path sanitization, there's always a potential that certain cleverly crafted paths 
         //     might bypass our first level of defense.
         //   - By ensuring that the resulting path is still within our root directory, we add another layer 
         //     of security that ensures that no external directories or files can be accessed.
         //Phase 2 - BEGIN
         std::filesystem::path rootPath(getRootDir());
         std::filesystem::path combinedPath = std::filesystem::absolute(rootPath / s);

         if (combinedPath.string().find(getRootDir()) != 0)
         {
             mg_http_reply(c, 403, "", "Oh you naughty, naughty Boy! (..)\n", (int)hm->uri.len, hm->uri.ptr);
             return false;
         }
         //Phase 2 - END

         // SECURITY - END