Files
MosisService/SANDBOX_MILESTONE_9.md

14 KiB

Milestone 9: Network - HTTP

Status: Complete Goal: Secure HTTP requests with domain filtering and SSRF prevention.


Overview

This milestone implements secure HTTP networking for Lua apps:

  • HTTPS required (no plain HTTP)
  • SSRF prevention (block private IPs, localhost, metadata endpoints)
  • Domain whitelist from app manifest
  • Request/response size limits
  • Concurrent request limits

Key Deliverables

  1. HttpValidator class - URL parsing, domain validation, IP blocking
  2. NetworkManager class - HTTP client with limits
  3. Lua network API - network.request() function
  4. Desktop mock server - For testing without network

File Structure

src/main/cpp/sandbox/
├── http_validator.h        # NEW - URL validation
├── http_validator.cpp      # NEW - SSRF prevention
├── network_manager.h       # NEW - HTTP client wrapper
└── network_manager.cpp     # NEW - Request execution

Implementation Details

1. URL Validation Rules

Allowed:
  https://api.example.com/path
  https://192.0.2.1/api  (public IP)

Blocked:
  http://example.com/...           # No plain HTTP
  https://127.0.0.1/...            # Localhost
  https://localhost/...            # Localhost name
  https://10.0.0.1/...             # Private IP (10.x.x.x)
  https://192.168.1.1/...          # Private IP (192.168.x.x)
  https://172.16.0.1/...           # Private IP (172.16-31.x.x)
  https://169.254.169.254/...      # AWS metadata
  https://[::1]/...                # IPv6 localhost
  https://[fc00::1]/...            # IPv6 private
  https://0.0.0.0/...              # All interfaces
  file:///etc/passwd              # File scheme
  ftp://ftp.example.com/...       # Non-HTTP schemes

2. HttpValidator Class

// http_validator.h
#pragma once

#include <string>
#include <vector>
#include <optional>

namespace mosis {

struct ParsedUrl {
    std::string scheme;      // "https"
    std::string host;        // "api.example.com" or "192.0.2.1"
    uint16_t port;           // 443
    std::string path;        // "/api/data"
    std::string query;       // "?key=value"
    bool is_ip_address;      // true if host is IP
};

class HttpValidator {
public:
    HttpValidator();

    // Set allowed domains (from app manifest)
    void SetAllowedDomains(const std::vector<std::string>& domains);

    // Clear domain restrictions (for testing)
    void ClearDomainRestrictions();

    // Validate URL
    // Returns parsed URL on success, sets error on failure
    std::optional<ParsedUrl> Validate(const std::string& url, std::string& error);

private:
    std::vector<std::string> m_allowed_domains;
    bool m_domain_restrictions_enabled;

    bool IsPrivateIP(const std::string& ip);
    bool IsLocalhostIP(const std::string& ip);
    bool IsMetadataIP(const std::string& ip);
    bool IsDomainAllowed(const std::string& host);
    std::optional<ParsedUrl> ParseUrl(const std::string& url);
};

} // namespace mosis

3. NetworkManager Class

// network_manager.h
#pragma once

#include <string>
#include <vector>
#include <map>
#include <functional>
#include <mutex>
#include <atomic>
#include "http_validator.h"

struct lua_State;

namespace mosis {

struct HttpRequest {
    std::string url;
    std::string method = "GET";
    std::map<std::string, std::string> headers;
    std::string body;
    int timeout_ms = 30000;
};

struct HttpResponse {
    int status_code = 0;
    std::map<std::string, std::string> headers;
    std::string body;
    std::string error;
};

struct NetworkLimits {
    size_t max_request_body = 10 * 1024 * 1024;     // 10 MB
    size_t max_response_body = 50 * 1024 * 1024;    // 50 MB
    int max_timeout_ms = 60000;                      // 60 seconds
    int max_concurrent_requests = 6;
    int default_timeout_ms = 30000;
};

class NetworkManager {
public:
    NetworkManager(const std::string& app_id, const NetworkLimits& limits = NetworkLimits{});
    ~NetworkManager();

    // Configure domain restrictions
    void SetAllowedDomains(const std::vector<std::string>& domains);
    void ClearDomainRestrictions();

    // Synchronous request (for testing)
    HttpResponse Request(const HttpRequest& request, std::string& error);

    // Stats
    int GetActiveRequestCount() const;

    // Get validator for testing
    HttpValidator& GetValidator() { return m_validator; }

private:
    std::string m_app_id;
    NetworkLimits m_limits;
    HttpValidator m_validator;
    std::atomic<int> m_active_requests{0};
    std::mutex m_mutex;

    HttpResponse ExecuteRequest(const ParsedUrl& parsed, const HttpRequest& request);
};

// Register network.* APIs as globals
void RegisterNetworkAPI(lua_State* L, NetworkManager* manager);

} // namespace mosis

4. Private IP Ranges (SSRF Prevention)

Range Description
127.0.0.0/8 Loopback
10.0.0.0/8 Private Class A
172.16.0.0/12 Private Class B
192.168.0.0/16 Private Class C
169.254.0.0/16 Link-local
169.254.169.254 Cloud metadata
0.0.0.0 All interfaces
::1 IPv6 loopback
fc00::/7 IPv6 private
fe80::/10 IPv6 link-local

5. Lua API

-- Simple GET request
local response, err = network.request({
    url = "https://api.example.com/data"
})
if not response then
    print("Error: " .. err)
    return
end
print("Status:", response.status)
print("Body:", response.body)

-- POST with headers and body
local response = network.request({
    url = "https://api.example.com/submit",
    method = "POST",
    headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer token123"
    },
    body = '{"key": "value"}',
    timeout = 30000  -- 30 seconds
})

-- Response structure
-- response.status   - HTTP status code (200, 404, etc.)
-- response.headers  - Response headers table
-- response.body     - Response body string
-- response.error    - Error message (if request failed)

6. Desktop Implementation

For desktop testing, we use cpp-httplib (header-only HTTP client).

On Android, we would use HttpURLConnection through JNI, but for the sandbox tests, we implement a mock HTTP server that returns predefined responses.

7. Mock Server for Testing

// Test helper - mock HTTP responses
class MockHttpServer {
public:
    void AddResponse(const std::string& url, const HttpResponse& response);
    std::optional<HttpResponse> GetResponse(const std::string& url);
    void Clear();
private:
    std::map<std::string, HttpResponse> m_responses;
};

Test Cases

Test 1: Block Private IPs

bool Test_NetworkBlocksPrivateIP(std::string& error_msg) {
    mosis::NetworkManager manager("test.app");
    manager.ClearDomainRestrictions();

    std::string err;

    // All these should be blocked
    std::vector<std::string> private_urls = {
        "https://127.0.0.1/api",
        "https://10.0.0.1/api",
        "https://192.168.1.1/api",
        "https://172.16.0.1/api",
        "https://169.254.169.254/latest/meta-data/",
        "https://localhost/api",
        "https://0.0.0.0/api"
    };

    for (const auto& url : private_urls) {
        mosis::HttpRequest req;
        req.url = url;
        auto response = manager.Request(req, err);
        EXPECT_TRUE(response.status_code == 0);  // Request should fail
        EXPECT_TRUE(err.find("blocked") != std::string::npos ||
                    err.find("private") != std::string::npos ||
                    err.find("localhost") != std::string::npos);
    }

    return true;
}

Test 2: Block Plain HTTP

bool Test_NetworkBlocksPlainHttp(std::string& error_msg) {
    mosis::NetworkManager manager("test.app");
    manager.ClearDomainRestrictions();

    std::string err;
    mosis::HttpRequest req;
    req.url = "http://example.com/api";  // No HTTPS

    auto response = manager.Request(req, err);
    EXPECT_TRUE(response.status_code == 0);
    EXPECT_TRUE(err.find("HTTPS") != std::string::npos);

    return true;
}

Test 3: Require HTTPS

bool Test_NetworkRequiresHttps(std::string& error_msg) {
    mosis::NetworkManager manager("test.app");
    manager.ClearDomainRestrictions();

    // HTTPS URL with mock validator should parse successfully
    std::string err;
    auto parsed = manager.GetValidator().Validate("https://example.com/api", err);
    EXPECT_TRUE(parsed.has_value());
    EXPECT_TRUE(parsed->scheme == "https");

    // HTTP should fail validation
    parsed = manager.GetValidator().Validate("http://example.com/api", err);
    EXPECT_FALSE(parsed.has_value());

    return true;
}

Test 4: Domain Whitelist

bool Test_NetworkEnforcesDomainWhitelist(std::string& error_msg) {
    mosis::NetworkManager manager("test.app");

    // Set allowed domains
    manager.SetAllowedDomains({"api.example.com", "cdn.example.com"});

    std::string err;

    // Allowed domain should validate
    auto parsed = manager.GetValidator().Validate("https://api.example.com/data", err);
    EXPECT_TRUE(parsed.has_value());

    // Disallowed domain should fail
    parsed = manager.GetValidator().Validate("https://evil.com/steal", err);
    EXPECT_FALSE(parsed.has_value());
    EXPECT_TRUE(err.find("allowed") != std::string::npos ||
                err.find("whitelist") != std::string::npos);

    return true;
}

Test 5: URL Parsing

bool Test_NetworkUrlParsing(std::string& error_msg) {
    mosis::HttpValidator validator;
    std::string err;

    // Full URL
    auto parsed = validator.Validate("https://api.example.com:8443/path/to/resource?key=value", err);
    EXPECT_TRUE(parsed.has_value());
    EXPECT_TRUE(parsed->scheme == "https");
    EXPECT_TRUE(parsed->host == "api.example.com");
    EXPECT_TRUE(parsed->port == 8443);
    EXPECT_TRUE(parsed->path == "/path/to/resource");
    EXPECT_TRUE(parsed->query == "?key=value");

    // Default port
    parsed = validator.Validate("https://example.com/api", err);
    EXPECT_TRUE(parsed.has_value());
    EXPECT_TRUE(parsed->port == 443);

    return true;
}

Test 6: Block Metadata Endpoints

bool Test_NetworkBlocksMetadata(std::string& error_msg) {
    mosis::NetworkManager manager("test.app");
    manager.ClearDomainRestrictions();

    std::string err;

    // AWS metadata
    mosis::HttpRequest req;
    req.url = "https://169.254.169.254/latest/meta-data/";
    auto response = manager.Request(req, err);
    EXPECT_TRUE(response.status_code == 0);

    // GCP metadata
    req.url = "https://metadata.google.internal/computeMetadata/v1/";
    response = manager.Request(req, err);
    EXPECT_TRUE(response.status_code == 0);

    return true;
}

Test 7: Request Limits

bool Test_NetworkRequestLimits(std::string& error_msg) {
    mosis::NetworkLimits limits;
    limits.max_request_body = 1024;  // 1 KB for testing

    mosis::NetworkManager manager("test.app", limits);
    manager.ClearDomainRestrictions();

    std::string err;
    mosis::HttpRequest req;
    req.url = "https://example.com/api";
    req.method = "POST";
    req.body = std::string(2048, 'X');  // 2 KB - exceeds limit

    auto response = manager.Request(req, err);
    EXPECT_TRUE(response.status_code == 0);
    EXPECT_TRUE(err.find("size") != std::string::npos ||
                err.find("limit") != std::string::npos ||
                err.find("large") != std::string::npos);

    return true;
}

Test 8: Lua Integration

bool Test_NetworkLuaIntegration(std::string& error_msg) {
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::NetworkManager manager("test.app");
    manager.ClearDomainRestrictions();
    mosis::RegisterNetworkAPI(sandbox.GetState(), &manager);

    std::string script = R"lua(
        -- Test that network global exists
        if not network then
            error("network global not found")
        end
        if not network.request then
            error("network.request not found")
        end

        -- Test validation rejection (private IP)
        local response, err = network.request({
            url = "https://127.0.0.1/api"
        })
        if response then
            error("expected private IP to be blocked")
        end
    )lua";

    bool ok = sandbox.LoadString(script, "network_test");
    if (!ok) {
        error_msg = "Lua test failed: " + sandbox.GetLastError();
        return false;
    }
    return true;
}

Acceptance Criteria

All tests must pass:

  • Test_NetworkBlocksPrivateIP - Private IPs blocked
  • Test_NetworkBlocksPlainHttp - Plain HTTP rejected
  • Test_NetworkRequiresHttps - HTTPS required
  • Test_NetworkEnforcesDomainWhitelist - Domain restrictions work
  • Test_NetworkUrlParsing - URL parsing correct
  • Test_NetworkBlocksMetadata - Cloud metadata blocked
  • Test_NetworkRequestLimits - Size limits enforced
  • Test_NetworkLuaIntegration - Lua API works

Dependencies

  • Milestone 1 (LuaSandbox)
  • Milestone 2 (PermissionGate)
  • Milestone 3 (RateLimiter, AuditLog)

Notes

Desktop vs Android Implementation

For the sandbox-test project (desktop), we focus on URL validation and request structure validation. Actual network requests would go through cpp-httplib or a mock server.

On Android, the real implementation would:

  1. Call through JNI to Java HttpURLConnection
  2. Use Android's network security config
  3. Integrate with the permission system

Security Considerations

  1. DNS Rebinding: In production, re-validate IP after DNS resolution
  2. Redirects: Follow redirects only to allowed domains, max 5 redirects
  3. Content-Type: Validate response content-type matches expected
  4. Timeouts: Both connection and read timeouts

Next Steps

After Milestone 9 passes:

  1. Milestone 10: Network - WebSocket