move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
521
docs/SANDBOX_MILESTONE_9.md
Normal file
521
docs/SANDBOX_MILESTONE_9.md
Normal file
@@ -0,0 +1,521 @@
|
||||
# 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
|
||||
|
||||
```cpp
|
||||
// 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
|
||||
|
||||
```cpp
|
||||
// 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
|
||||
|
||||
```lua
|
||||
-- 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
|
||||
|
||||
```cpp
|
||||
// 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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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
|
||||
|
||||
```cpp
|
||||
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:
|
||||
|
||||
- [x] `Test_NetworkBlocksPrivateIP` - Private IPs blocked
|
||||
- [x] `Test_NetworkBlocksPlainHttp` - Plain HTTP rejected
|
||||
- [x] `Test_NetworkRequiresHttps` - HTTPS required
|
||||
- [x] `Test_NetworkEnforcesDomainWhitelist` - Domain restrictions work
|
||||
- [x] `Test_NetworkUrlParsing` - URL parsing correct
|
||||
- [x] `Test_NetworkBlocksMetadata` - Cloud metadata blocked
|
||||
- [x] `Test_NetworkRequestLimits` - Size limits enforced
|
||||
- [x] `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
|
||||
Reference in New Issue
Block a user