Skip to content

Fix Security Issues #3

@mrhegemon

Description

@mrhegemon

Action Items

Immediate (P0)

  • Add request size limits and timeouts
  • Implement input validation for JSON parsing
  • Fix HTTP parsing vulnerabilities
  • Remove hardcoded credentials
  • Add basic authentication mechanism

High Priority (P1)

  • Fix path traversal vulnerability
  • Add security headers
  • Implement rate limiting
  • Add connection limits
  • Fix CORS configuration

1. HTTP Server Security Analysis

1.1 CRITICAL: Buffer Overflow Vulnerabilities

Location: src/server.cpp:390-428 (handle_client function)

Issue: The HTTP request parsing uses a fixed-size buffer with dynamic resizing, but several vulnerabilities exist:

std::vector<char> buffer(65536);
int total_read = 0;
int valread;

while ((valread = read(client_socket, buffer.data() + total_read, buffer.size() - total_read - 1)) > 0) {
    total_read += valread;
    buffer[total_read] = '\0';

Vulnerabilities:

  1. Unbounded read loop: An attacker can send unlimited data causing memory exhaustion
  2. Integer overflow: total_read can overflow if attacker sends > INT_MAX bytes
  3. No timeout on read: Slowloris-style attacks can exhaust server resources
  4. Buffer growth without limits: buffer.resize(buffer.size() * 2) can grow indefinitely

Impact:

  • Denial of Service (DoS) through memory exhaustion
  • Potential crashes from integer overflow
  • Resource exhaustion from hanging connections

Recommendation:

  1. Implement maximum request size limit (e.g., 10MB)
  2. Add read timeout using setsockopt(SO_RCVTIMEO)
  3. Add bounds checking on total_read before operations
  4. Implement connection limits per client IP

Example Fix:

const size_t MAX_REQUEST_SIZE = 10 * 1024 * 1024; // 10MB
const int READ_TIMEOUT_SEC = 30;

// Set socket timeout
struct timeval tv;
tv.tv_sec = READ_TIMEOUT_SEC;
tv.tv_usec = 0;
setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

// Check size limits
if (total_read >= MAX_REQUEST_SIZE) {
    std::cerr << "Request too large" << std::endl;
    send_error_response(client_socket, 413, "Request Entity Too Large");
    close(client_socket);
    return;
}

1.2 CRITICAL: HTTP Request Parsing Vulnerabilities

Location: src/server.cpp:57-87 (parse_http_request function)

Issue: Multiple vulnerabilities in HTTP header parsing:

// Parse body based on Content-Length
if (req.headers.find("Content-Length") != req.headers.end()) {
    int content_length = std::stoi(req.headers["Content-Length"]);
    req.body.resize(content_length);
    stream.read(&req.body[0], content_length);
}

Vulnerabilities:

  1. No validation of Content-Length value: Accepts negative or extremely large values
  2. No exception handling: std::stoi throws on invalid input, causing crash
  3. Direct trust of header: No sanity checking before memory allocation
  4. Missing bounds checking: Can allocate arbitrary amounts of memory

Impact:

  • Application crash from invalid Content-Length headers
  • Memory exhaustion from malicious Content-Length values
  • DoS attacks

Recommendation:

try {
    if (req.headers.find("Content-Length") != req.headers.end()) {
        long content_length = std::stol(req.headers["Content-Length"]);
        
        // Validate Content-Length
        if (content_length < 0 || content_length > MAX_REQUEST_SIZE) {
            std::cerr << "Invalid Content-Length: " << content_length << std::endl;
            return req; // Return empty request
        }
        
        req.body.resize(static_cast<size_t>(content_length));
        stream.read(&req.body[0], content_length);
    }
} catch (const std::exception& e) {
    std::cerr << "Error parsing Content-Length: " << e.what() << std::endl;
    // Return request with empty body
}

1.3 HIGH: Path Traversal Vulnerability

Location: src/server.cpp:126-165 (serve_static_file function)

Issue: Weak path traversal protection:

// Security check - prevent directory traversal
if (file_path.find("..") != std::string::npos) {
    return build_http_response(403, "text/plain", "Forbidden");
}

Vulnerabilities:

  1. Bypassable with URL encoding: %2e%2e bypasses the check
  2. Bypassable with alternate encodings: ..%2f, %2e%2e/, etc.
  3. No canonical path validation: Doesn't resolve symlinks or normalize paths
  4. Missing null byte checks: Can be exploited on some filesystems

Impact:

  • Read arbitrary files on the server
  • Information disclosure
  • Potential access to sensitive configuration files

Recommendation:

#include <filesystem>

std::string serve_static_file(const std::string& path) {
    namespace fs = std::filesystem;
    
    try {
        std::string file_path = path;
        if (file_path == "/") {
            file_path = "/index.html";
        }
        
        // Remove leading slash
        if (!file_path.empty() && file_path[0] == '/') {
            file_path = file_path.substr(1);
        }
        
        // Construct full path
        fs::path requested_path = fs::path("public") / file_path;
        fs::path base_path = fs::canonical("public");
        
        // Resolve to canonical path and validate it's within public/
        fs::path canonical_path = fs::canonical(requested_path);
        
        // Check if canonical path starts with base_path
        auto [base_end, path_end] = std::mismatch(
            base_path.begin(), base_path.end(),
            canonical_path.begin(), canonical_path.end()
        );
        
        if (base_end != base_path.end()) {
            return build_http_response(403, "text/plain", "Forbidden");
        }
        
        // ... rest of file serving logic
    } catch (const fs::filesystem_error& e) {
        return build_http_response(404, "text/plain", "Not Found");
    }
}

1.4 HIGH: Missing HTTP Security Headers

Location: src/server.cpp:36-47 (build_http_response function)

Issue: Critical security headers are missing:

std::string build_http_response(int status_code, const std::string& content_type, const std::string& body) {
    std::ostringstream response;
    response << "HTTP/1.1 " << status_code << " OK\r\n";
    response << "Content-Type: " << content_type << "\r\n";
    response << "Content-Length: " << body.length() << "\r\n";
    response << "Access-Control-Allow-Origin: *\r\n";
    // Missing critical security headers

Missing Headers:

  1. X-Frame-Options: Allows clickjacking attacks
  2. X-Content-Type-Options: Allows MIME sniffing attacks
  3. Content-Security-Policy: No CSP protection
  4. X-XSS-Protection: No XSS protection header
  5. Strict-Transport-Security: No HSTS (if served over HTTPS via nginx)

Impact:

  • Clickjacking attacks
  • Cross-site scripting (XSS) vulnerabilities
  • MIME sniffing attacks
  • Mixed content issues

Recommendation:

std::string build_http_response(int status_code, const std::string& content_type, const std::string& body) {
    std::ostringstream response;
    response << "HTTP/1.1 " << status_code << " OK\r\n";
    response << "Content-Type: " << content_type << "\r\n";
    response << "Content-Length: " << body.length() << "\r\n";
    
    // Security headers
    response << "X-Frame-Options: DENY\r\n";
    response << "X-Content-Type-Options: nosniff\r\n";
    response << "X-XSS-Protection: 1; mode=block\r\n";
    response << "Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'\r\n";
    response << "Referrer-Policy: strict-origin-when-cross-origin\r\n";
    
    // CORS (keep restrictive or configure based on needs)
    response << "Access-Control-Allow-Origin: *\r\n";
    response << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
    response << "Access-Control-Allow-Headers: Content-Type\r\n";
    response << "\r\n";
    response << body;
    return response.str();
}

1.5 CRITICAL: Race Condition in Conversation History

Location: src/server.cpp:96-101, 195-210, 293-308 (Conversation history management)

Issue: Potential race conditions in conversation history cleanup:

std::map<std::string, ConversationHistory> conversations;
std::mutex conversations_mutex;

// Later in code:
{
    std::lock_guard<std::mutex> lock(conversations_mutex);
    auto& conv = conversations[conversation_id];
    history = conv.messages;
    
    // Clean up old conversations (older than 1 hour)
    auto now = std::chrono::system_clock::now();
    for (auto it = conversations.begin(); it != conversations.end(); ) {
        if (std::chrono::duration_cast<std::chrono::hours>(now - it->second.last_activity).count() > 1) {
            it = conversations.erase(it);
        } else {
            ++it;
        }
    }
}

Vulnerabilities:

  1. Iterator invalidation during concurrent access: If multiple threads iterate and erase
  2. Use-after-free risk: History copied while cleanup may erase the same conversation
  3. Memory leak: Unlimited conversations can accumulate before cleanup
  4. No maximum conversation limit: Attackers can exhaust memory by creating many conversations

Impact:

  • Application crash from iterator invalidation
  • Memory exhaustion from conversation flooding
  • Undefined behavior

Recommendation:

// Add maximum conversation limit
const size_t MAX_CONVERSATIONS = 1000;

// Separate cleanup function
void cleanup_old_conversations() {
    std::lock_guard<std::mutex> lock(conversations_mutex);
    auto now = std::chrono::system_clock::now();
    
    for (auto it = conversations.begin(); it != conversations.end(); ) {
        if (std::chrono::duration_cast<std::chrono::hours>(now - it->second.last_activity).count() > 1) {
            it = conversations.erase(it);
        } else {
            ++it;
        }
    }
    
    // Enforce maximum limit by removing oldest if needed
    while (conversations.size() > MAX_CONVERSATIONS) {
        auto oldest = std::min_element(conversations.begin(), conversations.end(),
            [](const auto& a, const auto& b) {
                return a.second.last_activity < b.second.last_activity;
            });
        conversations.erase(oldest);
    }
}

// Call cleanup in a separate thread or less frequently

2. Authentication and Authorization

2.1 CRITICAL: No Authentication Mechanism

Location: All endpoints in src/server.cpp

Issue: The server has no authentication or authorization:

  • /query endpoint is completely open
  • /status endpoint is open
  • Static file serving is open
  • No API keys, tokens, or user authentication

Impact:

  • Unrestricted access to LLM queries
  • Resource exhaustion from abuse
  • Potential for malicious queries
  • No rate limiting or abuse prevention

Recommendation:

  1. Implement API key authentication for /query endpoint
  2. Add rate limiting per IP address
  3. Consider implementing basic HTTP authentication for production
  4. Add request throttling

Example Implementation:

// Add to config.h
const std::string API_KEY_HEADER = "X-API-Key";
const size_t MAX_REQUESTS_PER_MINUTE = 60;

// Add rate limiting map
std::map<std::string, std::pair<std::chrono::system_clock::time_point, int>> rate_limits;
std::mutex rate_limit_mutex;

bool check_rate_limit(const std::string& client_ip) {
    std::lock_guard<std::mutex> lock(rate_limit_mutex);
    auto now = std::chrono::system_clock::now();
    auto& [last_reset, count] = rate_limits[client_ip];
    
    // Reset counter every minute
    if (std::chrono::duration_cast<std::chrono::minutes>(now - last_reset).count() >= 1) {
        last_reset = now;
        count = 0;
    }
    
    if (count >= MAX_REQUESTS_PER_MINUTE) {
        return false;
    }
    
    count++;
    return true;
}

// In handle_client, add authentication check:
if (request.method == "POST" && request.path == "/query") {
    // Check API key if configured
    const char* required_key = getenv("API_KEY");
    if (required_key && required_key[0] != '\0') {
        auto key_header = request.headers.find(API_KEY_HEADER);
        if (key_header == request.headers.end() || key_header->second != required_key) {
            response = build_http_response(401, "application/json", 
                "{\"error\":\"Unauthorized\"}");
            send(client_socket, response.c_str(), response.length(), 0);
            close(client_socket);
            return;
        }
    }
    
    // Check rate limit
    if (!check_rate_limit(client_ip)) {
        response = build_http_response(429, "application/json",
            "{\"error\":\"Too Many Requests\"}");
        send(client_socket, response.c_str(), response.length(), 0);
        close(client_socket);
        return;
    }
    
    response = handle_query(request.body);
}

2.2 HIGH: CORS Configuration Too Permissive

Location: src/server.cpp:41

Issue:

response << "Access-Control-Allow-Origin: *\r\n";

Impact:

  • Any website can make requests to the server
  • Potential for cross-site request forgery (CSRF) attacks
  • Data exfiltration from malicious sites

Recommendation:

// Configure allowed origins from environment
std::string get_allowed_origin() {
    const char* origin = getenv("ALLOWED_ORIGIN");
    return origin ? origin : "http://localhost:8080";
}

// In build_http_response:
response << "Access-Control-Allow-Origin: " << get_allowed_origin() << "\r\n";
response << "Access-Control-Allow-Credentials: true\r\n";

3. Data Handling and Input Validation

3.1 HIGH: JSON Parsing Without Validation

Location: src/server.cpp:168-189 (handle_query function)

Issue: Direct parsing of user JSON input without validation:

auto request_json = json::parse(body);
std::string query = request_json["query"];

Vulnerabilities:

  1. No exception handling for malformed JSON
  2. No validation of query field existence
  3. No validation of query length or content
  4. Potential for JSON injection

Impact:

  • Application crash from malformed JSON
  • DoS from extremely large queries
  • Injection attacks

Recommendation:

const size_t MAX_QUERY_LENGTH = 10000;

std::string handle_query(const std::string& body) {
    try {
        // Validate body size
        if (body.length() > MAX_QUERY_LENGTH * 2) {
            json error;
            error["error"] = "Request body too large";
            return build_http_response(413, "application/json", error.dump());
        }
        
        auto request_json = json::parse(body);
        
        // Validate required fields
        if (!request_json.contains("query") || !request_json["query"].is_string()) {
            json error;
            error["error"] = "Missing or invalid 'query' field";
            return build_http_response(400, "application/json", error.dump());
        }
        
        std::string query = request_json["query"];
        
        // Validate query length
        if (query.empty() || query.length() > MAX_QUERY_LENGTH) {
            json error;
            error["error"] = "Query too long or empty";
            return build_http_response(400, "application/json", error.dump());
        }
        
        // Sanitize query (remove control characters)
        query.erase(std::remove_if(query.begin(), query.end(),
            [](char c) { return c < 32 && c != '\n' && c != '\r' && c != '\t'; }),
            query.end());
        
        // ... rest of processing
        
    } catch (const json::parse_error& e) {
        json error;
        error["error"] = "Invalid JSON format";
        return build_http_response(400, "application/json", error.dump());
    } catch (const std::exception& e) {
        // ... existing error handling
    }
}

3.2 MEDIUM: Unsafe String Operations

Location: src/text_utils.h:26-118 (extract_text_with_tika function)

Issue: Potential issues with file handling:

std::stringstream buffer;
buffer << file.rdbuf();
std::string file_content = buffer.str();

Vulnerabilities:

  1. No memory limits on file reading
  2. Entire file loaded into memory
  3. Can cause OOM with malicious files

Impact:

  • Memory exhaustion
  • DoS attacks

Recommendation:

  • Implement streaming for large files
  • Add memory monitoring
  • Implement file size limits (already present but can be improved)

3.3 MEDIUM: Command Injection via File Paths

Location: src/text_utils.h:27-59, src/server.cpp:146-157

Issue: File paths from user input used without sanitization

Recommendation:

  • Validate all file paths against allowed characters
  • Use allowlist of allowed file extensions
  • Implement strict path validation

4. Memory Management

4.1 HIGH: Resource Leaks in Error Paths

Location: src/server.cpp:528-535 (handle_client thread creation)

Issue:

try {
    std::thread client_thread(handle_client, client_socket);
    client_thread.detach();
} catch (const std::exception& e) {
    std::cerr << "Error creating client thread: " << e.what() << std::endl;
    close(client_socket);
}

Vulnerabilities:

  1. Socket not closed on thread creation failure
  2. Unlimited thread creation can exhaust resources
  3. No thread pool or connection limits

Impact:

  • Resource exhaustion
  • DoS attacks
  • File descriptor leaks

Recommendation:

const size_t MAX_CONCURRENT_CONNECTIONS = 100;
std::atomic<size_t> active_connections{0};

// In main loop:
if (active_connections >= MAX_CONCURRENT_CONNECTIONS) {
    std::cerr << "Too many concurrent connections" << std::endl;
    close(client_socket);
    continue;
}

active_connections++;

try {
    std::thread client_thread([client_socket]() {
        try {
            handle_client(client_socket);
        } catch (...) {
            close(client_socket);
        }
        active_connections--;
    });
    client_thread.detach();
} catch (const std::exception& e) {
    std::cerr << "Error creating client thread: " << e.what() << std::endl;
    close(client_socket);
    active_connections--;
}

4.2 MEDIUM: Memory Safety in LLM Context

Location: src/llm.h:49-214 (LLMGenerator class)

Issue: Context recreation on every request can fail

llama_free(ctx);
ctx = llama_init_from_model(model, ctx_params);

Vulnerabilities:

  1. No rollback if context creation fails
  2. Old context freed before new one created
  3. Can leave generator in broken state

Recommendation:

// Create new context first, then swap
llama_context* new_ctx = llama_init_from_model(model, ctx_params);
if (!new_ctx) {
    std::cerr << "Failed to create new context, keeping old one" << std::endl;
    // Keep old context and return error
    return "Error: Failed to create LLM context";
}

// Swap contexts
llama_context* old_ctx = ctx;
ctx = new_ctx;
llama_free(old_ctx);

5. Configuration and Secrets Management

5.1 CRITICAL: Hardcoded Credentials

Location: src/config.h:60-68 (PostgreSQL configuration)

Issue:

inline std::string get_pg_password() {
    const char* p = getenv("POSTGRES_PASSWORD");
    return p ? p : "jic_password";
}

Vulnerabilities:

  1. Default password hardcoded in source
  2. Credentials in plaintext
  3. No secrets rotation capability

Impact:

  • Unauthorized database access if defaults used
  • Credentials in version control
  • Compliance violations

Recommendation:

inline std::string get_pg_password() {
    const char* p = getenv("POSTGRES_PASSWORD");
    if (!p || p[0] == '\0') {
        std::cerr << "ERROR: POSTGRES_PASSWORD environment variable must be set" << std::endl;
        std::cerr << "No default password available for security reasons" << std::endl;
        exit(1);
    }
    return p;
}

5.2 HIGH: Insecure Model Path Handling

Location: src/config.h:14-41

Issue: Model paths constructed from environment variables without validation

Recommendation:

  • Validate model paths exist before use
  • Restrict to specific directories
  • Use absolute paths only

5.3 MEDIUM: Missing TLS/HTTPS Support

Issue: The C++ server doesn't support HTTPS natively, relies on nginx proxy

Recommendation:

  • Document that TLS must be handled by nginx
  • Add headers to indicate when behind reverse proxy
  • Consider adding native TLS support using OpenSSL

6. Error Handling and Logging

6.1 MEDIUM: Information Disclosure in Error Messages

Location: Multiple locations with std::cerr logging

Issue: Detailed error messages logged to stderr can expose system information

Recommendation:

  • Implement proper logging levels
  • Don't expose internal paths or system info in production
  • Use structured logging

6.2 LOW: No Request Logging for Security Auditing

Issue: No logging of requests for security monitoring

Recommendation:

void log_request(const std::string& client_ip, const HttpRequest& req) {
    auto now = std::chrono::system_clock::now();
    auto time = std::chrono::system_clock::to_time_t(now);
    
    std::cout << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] "
              << client_ip << " " 
              << req.method << " " 
              << req.path << std::endl;
}

7. Denial of Service Vulnerabilities

7.1 HIGH: No Connection Limits

Issue: Server accepts unlimited connections

Impact: DoS through connection exhaustion

Recommendation: Implement connection pooling and limits (see 4.1)


7.2 HIGH: No Query Complexity Limits

Location: src/server.cpp:168-371 (handle_query)

Issue:

  • No limits on context size for LLM
  • Can exhaust GPU/CPU resources
  • No timeout on LLM generation

Recommendation:

  • Implement query timeout
  • Limit LLM generation time
  • Add resource monitoring

7.3 MEDIUM: Vector Index Load Without Validation

Location: src/server.cpp:104-123 (load_index function)

Issue: Index loaded without size or integrity validation

Recommendation:

  • Validate index file size
  • Check file integrity
  • Implement corruption detection

8. Third-Party Dependencies

8.1 MEDIUM: Tika Service Communication

Location: src/text_utils.h:27-118

Issue: Communication with Tika service over HTTP without authentication

Recommendation:

  • Use HTTPS for Tika communication
  • Implement authentication between services
  • Validate Tika responses

8.2 LOW: Dependency Version Management

Issue: No explicit version pinning for llama.cpp in Dockerfile

Recommendation:

  • Pin specific commits or tags
  • Implement dependency scanning
  • Regular security updates

9. Summary of Findings

Critical Issues (Immediate Action Required):

  1. ✅ Buffer overflow vulnerabilities in HTTP parsing
  2. ✅ HTTP request parsing without validation
  3. ✅ No authentication mechanism
  4. ✅ Race condition in conversation history
  5. ✅ Hardcoded database credentials
  6. ✅ Missing input validation on JSON parsing
  7. ✅ Path traversal vulnerability

High Priority Issues:

  1. ✅ Path traversal protection weaknesses
  2. ✅ Missing HTTP security headers
  3. ✅ CORS configuration too permissive
  4. ✅ Resource leaks in error paths
  5. ✅ No connection limits or rate limiting

Medium Priority Issues:

  1. ✅ Unsafe string operations
  2. ✅ Memory safety in LLM context
  3. ✅ Missing TLS/HTTPS support
  4. ✅ Information disclosure in errors

10. Remediation Roadmap

Phase 1: Critical Fixes (Week 1)

  1. Implement request size limits
  2. Add input validation for all user inputs
  3. Fix HTTP parsing vulnerabilities
  4. Add basic authentication
  5. Remove hardcoded credentials

Phase 2: High Priority (Week 2-3)

  1. Implement proper path validation
  2. Add security headers
  3. Fix CORS configuration
  4. Add rate limiting
  5. Implement connection limits

Phase 3: Hardening (Week 4+)

  1. Add comprehensive logging
  2. Implement resource monitoring
  3. Add health checks
  4. Security testing and penetration testing
  5. Documentation updates

11. Secure Coding Practices Recommendations

11.1 Input Validation

  • Always validate and sanitize all inputs
  • Use allowlists rather than denylists
  • Validate data types, lengths, and formats
  • Implement proper error handling

11.2 Memory Safety

  • Use RAII and smart pointers where possible
  • Avoid raw pointers and manual memory management
  • Implement bounds checking
  • Use safe string operations

11.3 Error Handling

  • Never trust user input
  • Fail securely (deny by default)
  • Don't expose internal details in errors
  • Log security-relevant events

11.4 Authentication & Authorization

  • Implement strong authentication
  • Use API keys or tokens
  • Rate limit all endpoints
  • Implement proper session management

11.5 Network Security

  • Use TLS for all communications
  • Implement proper timeout handling
  • Use secure defaults
  • Validate all network inputs

12. nginx Configuration Recommendations

Since the application will be deployed behind nginx, add the following nginx configuration:

# Rate limiting
limit_req_zone $binary_remote_addr zone=jic:10m rate=10r/s;
limit_req zone=jic burst=20 nodelay;

# Connection limits
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10;

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    
    # TLS configuration
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Content-Security-Policy "default-src 'self'" always;
    
    # Request size limits
    client_max_body_size 10M;
    client_body_timeout 30s;
    client_header_timeout 30s;
    
    # Proxy settings
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeouts
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 90s;
    }
    
    # Block common attack patterns
    location ~ /\.(?!well-known) {
        deny all;
    }
}

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions