Preuve · code, extrait

api_key_access.go

Dépôt apteva/server · commit b1d72aa5f0326d40536b7cdd280733edeb9a285b · plateforme Apteva, serveur, clone du 13 septembre 2026

Extrait tel qu'à ce commit : lignes 27–116, sur 116 (numérotation d'origine, 30 lignes de contexte autour de chaque citation) · fichier intégral sur GitHub (dépôt public)

Lignes citées par l'audit : 57–86

90 lignes reproduites · ← retour à l'audit · catalogue des preuves

27func privateRequestToken(r *http.Request) string {
28	if a := r.Header.Get("Authorization"); a != "" {
29		return strings.TrimPrefix(a, "Bearer ")
30	}
31	if token := r.Header.Get("X-API-Key"); token != "" {
32		return token
33	}
34	if apiKeyQueryAllowed(r) {
35		return r.URL.Query().Get("api_key")
36	}
37	if appTokenQueryAllowed(r) {
38		token := r.URL.Query().Get("api_key")
39		if strings.HasPrefix(token, "app_") || strings.HasPrefix(token, "dev-") {
40			return token
41		}
42	}
43	return ""
44}
45
46// This is an allowlist, not a GET-method blanket permission. New endpoints,
47// credential exports, arbitrary app/core proxies and protocol upgrades must not
48// silently gain access. Existing handler-level user/project checks still apply.
49func readOnlyAPIRequestAllowed(r *http.Request) bool {
50	if r.Method != http.MethodGet || r.Header.Get("Upgrade") != "" {
51		return false
52	}
53	p := r.URL.Path // authMiddleware runs after /api is stripped
54	if path.Clean(p) != p || strings.ContainsAny(p, "\\%") {
55		return false
56	}
57	switch p {
58	case "/auth/me", "/auth/keys", "/auth/onboarding/status",
59		"/projects", "/agents", "/instances",
60		"/telemetry", "/telemetry/timeline", "/telemetry/stats",
61		"/telemetry/project-activity", "/telemetry/project-stats",
62		"/telemetry/project-timeline", "/telemetry/project-tools", "/telemetry/stream",
63		"/settings/server", "/settings/new-agent-provider":
64		return true
65	}
66	parts := strings.Split(strings.TrimPrefix(p, "/"), "/")
67	if len(parts) == 2 && parts[0] == "projects" && parts[1] != "" {
68		return true
69	}
70	if len(parts) < 2 || (parts[0] != "agents" && parts[0] != "instances") {
71		return false
72	}
73	if id, err := strconv.ParseInt(parts[1], 10, 64); err != nil || id <= 0 {
74		return false
75	}
76	if len(parts) == 2 {
77		return true
78	}
79	if len(parts) != 3 {
80		return false
81	}
82	switch parts[2] {
83	case "status", "threads", "events", "chat-history", "config":
84		return true
85	}
86	return false
87}
88
89// Configuration can contain executable commands, environment variables and
90// provider/MCP credentials. Expose only reviewed scalar settings to read-only
91// callers; arbitrary nested config is intentionally not an inspection surface.
92func readOnlyAgentConfig(config map[string]any) map[string]any {
93	out := map[string]any{}
94	for _, key := range []string{
95		"directive", "mode", "proactivity", "model", "provider", "default_provider",
96		"rate", "temperature", "max_tokens", "context_size", "reasoning_effort",
97		"background_memory_enabled", "realtime_enabled",
98	} {
99		switch value := config[key].(type) {
100		case string, float64, int, bool:
101			out[key] = value
102		}
103	}
104	return out
105}
106
107func restrictAgentConfig(r *http.Request, agent *Agent) {
108	if !isReadOnlyAPIKey(r) {
109		return
110	}
111	var config map[string]any
112	_ = json.Unmarshal([]byte(agent.Config), &config)
113	raw, _ := json.Marshal(readOnlyAgentConfig(config))
114	agent.Config = string(raw)
115}
116