Skip to content

Commit 388f8dc

Browse files
authored
Merge pull request #794 from airweave-ai/feat/cred_sec
vuln: don't log credentials
2 parents c6660f7 + 19dcdce commit 388f8dc

File tree

6 files changed

+307
-21
lines changed

6 files changed

+307
-21
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""Credential sanitization utilities for safe logging.
2+
3+
This module provides functions to safely log credential information without
4+
exposing sensitive data like API keys, tokens, passwords, etc.
5+
"""
6+
7+
import re
8+
from typing import Any, Callable, Dict
9+
10+
11+
def sanitize_credential_value(value: Any, show_length: bool = True) -> str:
12+
"""Sanitize a credential value for safe logging.
13+
14+
Args:
15+
value: The credential value to sanitize
16+
show_length: Whether to show the length of the value
17+
18+
Returns:
19+
A sanitized string representation of the value
20+
"""
21+
if isinstance(value, str):
22+
if len(value) <= 8:
23+
return f"<redacted:{len(value)} chars>"
24+
else:
25+
preview = f"{value[:3]}...{value[-2:]}"
26+
if show_length:
27+
return f"<redacted:{len(value)} chars:{preview}>"
28+
else:
29+
return f"<redacted:{preview}>"
30+
elif isinstance(value, (int, float)):
31+
return f"<redacted {type(value).__name__}>"
32+
elif isinstance(value, bool):
33+
return f"<redacted bool:{value}>"
34+
elif value is None:
35+
return "<redacted:null>"
36+
else:
37+
return f"<redacted {type(value).__name__}>"
38+
39+
40+
def sanitize_credentials_dict(
41+
credentials: Dict[str, Any], show_lengths: bool = True
42+
) -> Dict[str, str]:
43+
"""Sanitize a dictionary of credentials for safe logging.
44+
45+
Args:
46+
credentials: Dictionary containing credential data
47+
show_lengths: Whether to show lengths of string values
48+
49+
Returns:
50+
Dictionary with sanitized values
51+
"""
52+
sanitized = {}
53+
for key, value in credentials.items():
54+
sanitized[key] = sanitize_credential_value(value, show_lengths)
55+
return sanitized
56+
57+
58+
def get_safe_credential_summary(credentials: Dict[str, Any]) -> str:
59+
"""Get a safe summary of credentials without exposing sensitive data.
60+
61+
Args:
62+
credentials: Dictionary containing credential data
63+
64+
Returns:
65+
A safe string summary of the credentials
66+
"""
67+
if not credentials:
68+
return "No credentials found"
69+
70+
# Count sensitive vs non-sensitive fields
71+
sensitive_fields = []
72+
non_sensitive_fields = []
73+
74+
for key in credentials.keys():
75+
if _is_sensitive_field(key):
76+
sensitive_fields.append(key)
77+
else:
78+
non_sensitive_fields.append(key)
79+
80+
summary_parts = [
81+
f"Total fields: {len(credentials)}",
82+
f"Sensitive fields: {len(sensitive_fields)}",
83+
f"Non-sensitive fields: {len(non_sensitive_fields)}",
84+
]
85+
86+
if non_sensitive_fields:
87+
summary_parts.append(f"Non-sensitive: {non_sensitive_fields}")
88+
89+
if sensitive_fields:
90+
summary_parts.append(f"Sensitive: {sensitive_fields}")
91+
92+
return " | ".join(summary_parts)
93+
94+
95+
def _is_sensitive_field(field_name: str) -> bool:
96+
"""Check if a field name indicates sensitive data.
97+
98+
Args:
99+
field_name: The name of the field to check
100+
101+
Returns:
102+
True if the field likely contains sensitive data
103+
"""
104+
sensitive_patterns = [
105+
r"token",
106+
r"key",
107+
r"secret",
108+
r"password",
109+
r"credential",
110+
r"auth",
111+
r"access",
112+
r"refresh",
113+
r"bearer",
114+
r"api_key",
115+
r"client_secret",
116+
r"private",
117+
r"session",
118+
r"cookie",
119+
]
120+
121+
field_lower = field_name.lower()
122+
return any(re.search(pattern, field_lower) for pattern in sensitive_patterns)
123+
124+
125+
def safe_log_credentials(
126+
credentials: Dict[str, Any],
127+
logger_func: Callable[[str], None],
128+
message_prefix: str = "",
129+
) -> None:
130+
"""Safely log credentials using the provided logger function.
131+
132+
Args:
133+
credentials: Dictionary containing credential data
134+
logger_func: Logger function to use (e.g., logger.info, logger.debug)
135+
message_prefix: Optional prefix for the log message
136+
"""
137+
summary = get_safe_credential_summary(credentials)
138+
if message_prefix:
139+
logger_func(f"{message_prefix} {summary}")
140+
else:
141+
logger_func(summary)
142+
143+
144+
def safe_log_credential_fields(
145+
credentials: Dict[str, Any],
146+
logger_func: Callable[[str], None],
147+
message_prefix: str = "",
148+
) -> None:
149+
"""Safely log credential field names and types without values.
150+
151+
Args:
152+
credentials: Dictionary containing credential data
153+
logger_func: Logger function to use
154+
message_prefix: Optional prefix for the log message
155+
"""
156+
if not credentials:
157+
logger_func(f"{message_prefix} No credential fields")
158+
return
159+
160+
field_info = []
161+
for key, value in credentials.items():
162+
field_type = type(value).__name__
163+
if isinstance(value, str):
164+
field_info.append(f"{key}({field_type}:{len(value)} chars)")
165+
else:
166+
field_info.append(f"{key}({field_type})")
167+
168+
fields_str = ", ".join(field_info)
169+
if message_prefix:
170+
logger_func(f"{message_prefix} Fields: {fields_str}")
171+
else:
172+
logger_func(f"Credential fields: {fields_str}")
173+
174+
175+
def safe_log_token_info(
176+
token: str, logger_func: Callable[[str], None], message_prefix: str = ""
177+
) -> None:
178+
"""Safely log token information without exposing the actual token.
179+
180+
Args:
181+
token: The token to log information about
182+
logger_func: Logger function to use
183+
message_prefix: Optional prefix for the log message
184+
"""
185+
if not token:
186+
logger_func(f"{message_prefix} No token provided")
187+
return
188+
189+
token_info = f"Length: {len(token)}"
190+
if len(token) > 10:
191+
token_info += f", Preview: {token[:3]}...{token[-3:]}"
192+
193+
if message_prefix:
194+
logger_func(f"{message_prefix} {token_info}")
195+
else:
196+
logger_func(f"Token info: {token_info}")
197+
198+
199+
def safe_log_auth_values(
200+
auth_values: Dict[str, Any],
201+
logger_func: Callable[[str], None],
202+
message_prefix: str = "",
203+
) -> None:
204+
"""Safely log auth values without exposing sensitive data.
205+
206+
Args:
207+
auth_values: Dictionary containing auth values
208+
logger_func: Logger function to use
209+
message_prefix: Optional prefix for the log message
210+
"""
211+
if not auth_values:
212+
logger_func(f"{message_prefix} No auth values")
213+
return
214+
215+
# Separate sensitive and non-sensitive fields
216+
sensitive_fields = []
217+
non_sensitive_fields = []
218+
219+
for key in auth_values.keys():
220+
if _is_sensitive_field(key):
221+
sensitive_fields.append(key)
222+
else:
223+
non_sensitive_fields.append(key)
224+
225+
info_parts = [
226+
f"Total: {len(auth_values)}",
227+
f"Sensitive: {len(sensitive_fields)}",
228+
f"Non-sensitive: {len(non_sensitive_fields)}",
229+
]
230+
231+
if non_sensitive_fields:
232+
info_parts.append(f"Non-sensitive fields: {non_sensitive_fields}")
233+
234+
if sensitive_fields:
235+
info_parts.append(f"Sensitive fields: {sensitive_fields}")
236+
237+
info_str = " | ".join(info_parts)
238+
if message_prefix:
239+
logger_func(f"{message_prefix} {info_str}")
240+
else:
241+
logger_func(f"Auth values: {info_str}")

backend/airweave/platform/auth_providers/composio.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import httpx
66
from fastapi import HTTPException
77

8+
from airweave.core.credential_sanitizer import (
9+
safe_log_credentials,
10+
sanitize_credentials_dict,
11+
)
812
from airweave.platform.auth.schemas import AuthType
913
from airweave.platform.auth_providers._base import BaseAuthProvider
1014
from airweave.platform.decorators import auth_provider
@@ -80,7 +84,10 @@ def _map_field_name(self, airweave_field: str) -> str:
8084
return self.FIELD_NAME_MAPPING.get(airweave_field, airweave_field)
8185

8286
async def _get_with_auth(
83-
self, client: httpx.AsyncClient, url: str, params: Optional[Dict[str, Any]] = None
87+
self,
88+
client: httpx.AsyncClient,
89+
url: str,
90+
params: Optional[Dict[str, Any]] = None,
8491
) -> Dict[str, Any]:
8592
"""Make authenticated API request using Composio API key.
8693
@@ -159,7 +166,11 @@ async def get_creds_for_source(
159166

160167
# TODO: pagination
161168

162-
self.logger.info(f"\n🔑 [Composio] Found credentials: {found_credentials}\n")
169+
safe_log_credentials(
170+
found_credentials,
171+
self.logger.info,
172+
f"\n🔑 [Composio] Retrieved credentials for '{source_short_name}':",
173+
)
163174
return found_credentials
164175

165176
async def _get_source_connected_accounts(
@@ -267,12 +278,13 @@ def _find_matching_connection(
267278
self.logger.info(
268279
f"\n🔓 [Composio] Available credential fields: {available_fields}\n"
269280
)
270-
for field, value in source_creds_dict.items():
271-
if isinstance(value, str) and len(value) > 10:
272-
preview = f"{value[:5]}...{value[-3:]}"
273-
else:
274-
preview = "<non-string or short value>"
275-
self.logger.debug(f"\n - {field}: {preview}\n")
281+
# Log credential fields safely without exposing values
282+
sanitized_preview = sanitize_credentials_dict(
283+
source_creds_dict, show_lengths=False
284+
)
285+
self.logger.debug(
286+
f"\n🔓 [Composio] Credential fields preview: {sanitized_preview}\n"
287+
)
276288
break
277289

278290
if not source_creds_dict:

backend/airweave/platform/auth_providers/pipedream.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import httpx
77
from fastapi import HTTPException
88

9+
from airweave.core.credential_sanitizer import safe_log_credentials
910
from airweave.platform.auth.schemas import AuthType
1011
from airweave.platform.auth_providers._base import BaseAuthProvider
1112
from airweave.platform.decorators import auth_provider
@@ -263,7 +264,11 @@ async def get_creds_for_source(
263264
account_data, source_auth_config_fields, source_short_name
264265
)
265266

266-
self.logger.info(f"\n🔑 [Pipedream] Found credentials: {found_credentials}\n")
267+
safe_log_credentials(
268+
found_credentials,
269+
self.logger.info,
270+
f"\n🔑 [Pipedream] Retrieved credentials for '{source_short_name}':",
271+
)
267272
return found_credentials
268273

269274
async def _get_account_with_credentials(

frontend/src/lib/auth-context.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
5959
setTokenInitialized(true);
6060
console.log('Auth initialization complete');
6161

62-
// Log the token for debugging
63-
console.log('Token length:', accessToken.length);
64-
// Print first and last 10 characters of token
65-
console.log('Token preview:', accessToken.substring(0, 10) + '...' + accessToken.substring(accessToken.length - 10));
62+
// Log token acquisition for debugging (without exposing token content)
63+
console.log('🔑 Access token acquired successfully, length:', accessToken.length);
6664
} catch (error) {
6765
console.error('Error getting access token', error);
6866
setToken(null);

frontend/src/pages/SemanticMcp.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,18 @@ const SemanticMcp = () => {
345345
}
346346
}, [searchParams, setSearchParams]);
347347

348-
// Log authValues whenever they change
348+
// Log authValues metadata whenever they change (without exposing sensitive data)
349349
useEffect(() => {
350-
console.log('🔐 [SemanticMcp] AuthValues updated:', authValues);
351-
console.log('🔐 [SemanticMcp] AuthValues keys:', Object.keys(authValues));
350+
const sensitiveFields = Object.keys(authValues).filter(key =>
351+
/(?:token|key|secret|password|credential)/i.test(key)
352+
);
353+
const nonSensitiveFields = Object.keys(authValues).filter(key =>
354+
!/(?:token|key|secret|password|credential)/i.test(key)
355+
);
356+
357+
console.log('🔐 [SemanticMcp] AuthValues updated - Total fields:', Object.keys(authValues).length,
358+
'Sensitive fields:', sensitiveFields.length,
359+
'Non-sensitive:', nonSensitiveFields);
352360
}, [authValues]);
353361

354362
// Log configValues whenever they change
@@ -458,7 +466,7 @@ const SemanticMcp = () => {
458466
auth_fields: authValues
459467
};
460468

461-
console.log('🔐 [SemanticMcp] Creating credentials for non-OAuth2 source:', credentialData);
469+
console.log('🔐 [SemanticMcp] Creating credentials for non-OAuth2 source:', sourceShortName, 'with', Object.keys(credentialData).length, 'fields');
462470

463471
// Make API call to create credentials
464472
const response = await apiClient.post(

0 commit comments

Comments
 (0)