Skip to main content
gitGost enforces multiple layers of security controls to prevent abuse, protect the service, and maintain GitHub’s trust. This page documents all rate limits, size restrictions, and validation checks.

Per-IP Rate Limiting

5 PRs per IP per hour—the primary abuse prevention mechanism.

Implementation

From handlers.go:559-562:
rateLimitStore  = make(map[string][]time.Time) // IP → timestamps
rateLimitWindow = time.Hour
rateLimitMaxPRs = 5

How It Works

1

Request Arrives

gitGost extracts the client IP from the TCP connection:
// handlers.go:160
ip := c.ClientIP()
2

Sliding Window Check

The system checks the last 1 hour of push attempts from this IP:
// handlers.go:736-749
now := time.Now()
times := rateLimitStore[ip]
valid := times[:0]
for _, t := range times {
    if now.Sub(t) < rateLimitWindow {
        valid = append(valid, t)
    }
}
valid = append(valid, now)
rateLimitStore[ip] = valid
count := len(valid)
3

Limit Enforcement

If count exceeds 5, the request is rejected:
// handlers.go:751-759
if count > rateLimitMaxPRs {
    // Reject with git protocol error
    WriteSidebandLine(&errResp, 2, "remote: Rate limit exceeded")
    WriteSidebandLine(&errResp, 3, "push rejected: rate limit exceeded")
    return
}
4

Admin Notification

On first excess (6th attempt), admin is notified via ntfy:
// handlers.go:752-755
if count == rateLimitMaxPRs+1 {
    go notifyAdminRateLimit(ip, count)
}

User Experience

When rate limit is exceeded, users see:
remote: 
remote: Rate limit exceeded: max 5 PRs per hour per IP.
remote: Please try again later.
remote: 
push rejected: rate limit exceeded

Bypass Protection

In-memory storage means rate limit resets on service restart. This is intentional:
  • Prevents disk I/O for every push (performance)
  • Automatically resets on legitimate service updates
  • Still effective against botnet attacks (which persist across restarts)
Cannot be bypassed by:
  • VPN/Tor rotation during the 1-hour window (IP still tracked)
  • Multiple git remotes (same IP)
  • Different repositories (per-IP, not per-repo)
Can be bypassed by:
  • Rotating IPs faster than the 1-hour window (requires botnet or many VPNs)
  • Waiting 1 hour between batches of 5 PRs

Push Size Limits

Maximum Push Size: 100 MB

From router.go:49:
const maxPushSize = 100 * 1024 * 1024 // 100MB
Enforced via middleware before push processing:
// router.go:52-59
func sizeLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.ContentLength > maxPushSize {
            c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge,
                gin.H{"error": "Push too large"})
            return
        }
        c.Next()
    }
}
Pushes larger than 100 MB are rejected with HTTP 413 (Request Entity Too Large).

Upload Pack Limit: 50 MB

For fetch/pull operations (less critical, but still limited):
// handlers.go:441
const maxUploadBytes = 50 * 1024 * 1024 // 50 MB

Why Size Limits?

gitGost processes git packs in-memory during anonymization. Large pushes could exhaust server memory.
Prevents attackers from DoS-ing the service with huge binary uploads.
GitHub has its own size limits. gitGost aligns with practical GitHub constraints.
100 MB is sufficient for:
  • Large feature branches
  • Multiple commits
  • Reasonable binary assets
Not sufficient for:
  • Video files, large datasets (use Git LFS)
  • Entire repository history dumps

User Experience

When size limit is exceeded:
curl -X POST -d @large-pack https://gitgost.leapcell.app/v1/gh/owner/repo/git-receive-pack
# → HTTP/1.1 413 Request Entity Too Large
# {"error": "Push too large"}
Git shows:
error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413
fatal: the remote end hung up unexpectedly

Repository Name Validation

Validation Rules

From router.go:78-93:
func isValidRepoName(name string) bool {
    // 1. Length check
    if len(name) == 0 || len(name) > 100 {
        return false
    }
    
    // 2. Character whitelist: alphanumeric + - _ .
    for _, r := range name {
        if !((r >= 'a' && r <= 'z') || 
             (r >= 'A' && r <= 'Z') || 
             (r >= '0' && r <= '9') || 
             r == '-' || r == '_' || r == '.') {
            return false
        }
    }
    
    // 3. Path traversal prevention
    if strings.Contains(name, "..") || strings.Contains(name, "/") {
        return false
    }
    
    return true
}

What’s Blocked

# Too long
gitgost.leapcell.app/v1/gh/user/this-repository-name-is-way-too-long-and-exceeds-the-maximum-allowed-length-of-one-hundred-characters
# → 400 Bad Request: Invalid repo name

# Path traversal attempt
gitgost.leapcell.app/v1/gh/../../../etc/passwd/repo
# → 400 Bad Request: Invalid repo name

# Special characters
gitgost.leapcell.app/v1/gh/user/repo@main
# → 400 Bad Request: Invalid repo name

# Slashes
gitgost.leapcell.app/v1/gh/user/my/nested/repo
# → 400 Bad Request: Invalid repo name

Why Strict Validation?

Security: Prevents path traversal attacks
Stability: Avoids file system issues with special characters
Compatibility: Aligns with GitHub’s repository naming rules

Admin Endpoint Rate Limiting

Admin endpoints (panic button, rollback) have stricter limits:

Panic Endpoint: 10 Requests/Minute/IP

From router.go:14-19:
adminLimiterStore = make(map[string][]time.Time)
adminLimiterMax   = 10
adminLimiterWin   = time.Minute
Applied to:
  • POST /admin/panic
  • POST /admin/rollback

Rollback Endpoint: 5 Requests/Minute

Additional limit specific to rollback:
// handlers.go:590-591
rollbackLimitMax   = 5
rollbackLimitWin   = time.Minute
This prevents accidental mass-closure of legitimate PRs.

Global Burst Detection

Automatic botnet detection across all IPs.

How It Works

From handlers.go:566-572:
globalBurstTimes    []time.Time        // timestamps of all pushes
globalBurstIPs      []string           // IPs corresponding to each push
globalBurstWindow   = 60 * time.Second // sliding window
globalBurstMaxTotal = 20               // max pushes globally
globalBurstMaxIPs   = 10               // max distinct IPs
1

Every Push is Recorded Globally

Regardless of IP:
// handlers.go:176
go recordGlobalBurst(ip)
2

Sliding Window Analysis

System checks if in the last 60 seconds:
  • 20+ total pushes (across all IPs), OR
  • 10+ distinct IPs pushed
3

Alert Trigger

If thresholds exceeded:
// handlers.go:695-698
if !globalBurstAlerted && 
   (total >= globalBurstMaxTotal || distinctIPs >= globalBurstMaxIPs) {
    globalBurstAlerted = true
    go notifyAdminGlobalBurst(total, distinctIPs)
}
4

Admin Response

Admin receives ntfy notification with action buttons:
  • Activate Panic Mode (suspend all pushes)
  • Close Burst PRs (rollback recent PRs)
  • Deactivate Panic Mode (resume service)

Why This Matters

Per-IP rate limiting alone is insufficient against distributed botnets.
Example attack:
  • Botnet with 100 IPs
  • Each IP pushes 4 PRs (under per-IP limit of 5)
  • Total: 400 PRs in 60 seconds
  • Per-IP limit didn’t trigger, but service is overwhelmed
Global burst detection catches this:
  • 400 pushes in 60s ≫ 20 threshold → Alert
  • 100 distinct IPs ≫ 10 threshold → Alert
  • Admin activates panic mode, rollback closes all 400 PRs

Panic Mode

Emergency service suspension when under attack.

Activation

From handlers.go:789-815:
func PanicHandler(c *gin.Context) {
    // Requires password OR single-use action token
    authorized := (panicPassword != "" && req.Password == panicPassword) ||
                  (req.Token != "" && consumeActionToken(req.Token))
    
    if !authorized {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
        return
    }
    
    panicMu.Lock()
    panicMode = req.Active
    panicMu.Unlock()
}

Effect

When panic mode is active:
// handlers.go:142-157
if isPanicMode() {
    WriteSidebandLine(&errResp, 2, "remote: SERVICE TEMPORARILY SUSPENDED")
    WriteSidebandLine(&errResp, 2, "remote: The panic button has been activated.")
    WriteSidebandLine(&errResp, 2, "remote: Please try again in 15 minutes.")
    WriteSidebandLine(&errResp, 3, "push rejected: service temporarily suspended")
    return
}
Users see:
remote: SERVICE TEMPORARILY SUSPENDED
remote: The panic button has been activated. The service has been
remote: temporarily suspended due to detected bot activity
remote: sending mass PRs. Please try again in 15 minutes.
push rejected: service temporarily suspended

Action Tokens (Security)

Single-use, time-limited tokens prevent password exposure in ntfy notifications.
From handlers.go:616-641:
func newActionToken() string {
    b := make([]byte, 16)
    rand.Read(b)
    token := hex.EncodeToString(b)
    expiry := time.Now().Add(actionTokenTTL) // 10 minutes
    actionTokens[token] = expiry
    return token
}

func consumeActionToken(token string) bool {
    expiry, ok := actionTokens[token]
    if !ok {
        return false
    }
    delete(actionTokens, token) // Single-use
    return time.Now().Before(expiry)
}
This prevents:
  • Password exposure in ntfy notification URLs
  • Token reuse by third parties who intercept notifications
  • Replay attacks (tokens expire in 10 minutes)

Rollback System

Close all PRs created during a burst with a single action.

How It Works

From handlers.go:826-908:
1

PR Registration During Bursts

When global burst is active, all new PRs are registered:
// handlers.go:332-349
if isGlobalBurstAlertActive() {
    recentBurstPRsMu.Lock()
    recentBurstPRs = append(recentBurstPRs, prURL)
    recentBurstPRsAt = append(recentBurstPRsAt, nowPR)
    recentBurstPRsMu.Unlock()
}
2

Admin Triggers Rollback

Via ntfy action button or direct API call:
curl -X POST https://gitgost.leapcell.app/admin/rollback \
  -H "Content-Type: application/json" \
  -d '{"token": "<action-token>"}'
3

Concurrent PR Closure

Up to 5 workers close PRs in parallel:
// handlers.go:874-900
const maxCloseWorkers = 5
for _, prURL := range toClose {
    go func(u string) {
        if err := github.ClosePRByURL(u); err != nil {
            failed = append(failed, u)
        } else {
            closed = append(closed, u)
        }
    }(prURL)
}
4

Response

Admin receives summary:
{
  "closed": 127,
  "failed": 3,
  "closed_urls": ["https://github.com/...", ...],
  "failed_urls": ["https://github.com/...", ...]
}

TTL: 2 Hours

PRs are only tracked for rollback for 2 hours:
// handlers.go:579
recentBurstPRsTTL = 2 * time.Hour
After 2 hours, PRs are no longer eligible for rollback (assumed legitimate if not flagged quickly).

Security Headers & Proxy Protection

Trusted Proxies: Disabled

From router.go:134:
r.SetTrustedProxies([]string{}) // Use real TCP IP, not proxy headers
Critical for privacy: gitGost does NOT trust X-Forwarded-For or similar headers.
This prevents:
  • Attackers from spoofing source IPs via proxy headers
  • Bypassing rate limits by injecting fake IPs
  • Correlation attacks via header injection
Trade-off: If gitGost runs behind a reverse proxy, c.ClientIP() returns the proxy’s IP, not the end user’s. This is acceptable because:
  1. Rate limiting still works (limits per-proxy-IP)
  2. Real user IP is never logged anyway (privacy by design)

Anonymous Authentication

Git operations require no authentication:
// router.go:99-105
func anonymousAuthMiddleware(apiKey string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Always allow git-receive-pack without authentication
        if strings.Contains(c.Request.URL.Path, "git-receive-pack") ||
           strings.Contains(c.Request.URL.Path, "git-upload-pack") ||
           strings.Contains(c.Request.URL.Path, "info/refs") {
            c.Next()
            return
        }
        // Other endpoints require API key (if configured)
    }
}
This ensures:
  • No user accounts or registration
  • No credentials to leak
  • True anonymous access

Summary Table

Limit TypeValueScopeEnforcement
Per-IP PR rate5/hourPer source IPSliding window, in-memory
Push size100 MBPer pushMiddleware, before processing
Upload size50 MBPer fetch/pullRequest body limit
Repo name length1-100 charsPer requestValidation middleware
Admin endpoint rate10/min/IPPer IPSliding window
Rollback rate5/minGlobalSliding window
Global burst (total)20/60sAll IPsAlert only (admin action)
Global burst (IPs)10/60sDistinct IPsAlert only (admin action)

Monitoring & Transparency

Public Metrics

Real-time service health (memory, goroutines, uptime)

Service Status

Check if panic mode is active

Total PRs

Aggregate PR count (no personal data)

Recent Activity

Last 10 PRs (owner, repo, URL only)

Threat Model

Attack vectors and mitigations

Privacy Guarantees

Data retention and logging policies

Anonymity Limits

When gitGost is NOT sufficient

Self-Hosting

Run your own instance with custom limits

Build docs developers (and LLMs) love