Skip to content

Commit 030eac5

Browse files
committed
#2979, #2437, #2247, #1836: Enable mysql cleartext auth without access to keychain and with a warning to the user
1 parent 586312b commit 030eac5

8 files changed

+245
-22
lines changed

Frameworks/SPMySQLFramework/Source/SPMySQLConnection.h

+18
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
pthread_t reconnectingThread;
6262
uint64_t initialConnectTime;
6363
unsigned long mysqlConnectionThreadId;
64+
BOOL allowCleartextPlugin;
6465

6566
// Connection proxy
6667
NSObject <SPMySQLConnectionProxy> *proxy;
@@ -175,6 +176,23 @@
175176
- (void)addClientFlags:(SPMySQLClientFlags)opts;
176177
- (void)removeClientFlags:(SPMySQLClientFlags)opts;
177178

179+
/**
180+
* This tells the mysql client whether the cleartext auth plugin is permitted.
181+
*
182+
* If enabled, and requested by the server, the password will simply be transmitted
183+
* in plaintext, instead of hashing it on the client first, thus this plugin is
184+
* rather risky. It is mostly used when the server has to forward the password to another
185+
* auth backend (which it can't do with the one-way hash).
186+
*
187+
* If it is not enabled, but requested by the server the connection will fail with
188+
* error "plugin not enabled (2059)"
189+
*
190+
* WARNING: There are 2 ways to enable this plugin: Via this property or by setting
191+
* the envvar LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN to 1. Sadly ANY ONE of those two is
192+
* sufficient to enable the plugin.
193+
*/
194+
@property (readwrite, assign, nonatomic) BOOL allowCleartextPlugin;
195+
178196
#pragma mark -
179197
#pragma mark Connection and disconnection
180198

Frameworks/SPMySQLFramework/Source/SPMySQLConnection.m

+8-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ @implementation SPMySQLConnection
8282
@synthesize delegateQueryLogging;
8383
@synthesize lastQueryWasCancelled;
8484
@synthesize clientFlags = clientFlags;
85+
@synthesize allowCleartextPlugin = allowCleartextPlugin;
8586

8687
#pragma mark -
8788
#pragma mark Getters and Setters
@@ -609,6 +610,9 @@ - (MYSQL *)_makeRawMySQLConnectionWithEncoding:(NSString *)encodingName isMaster
609610
NSStringEncoding connectEncodingNS = [SPMySQLConnection stringEncodingForMySQLCharset:[encodingName UTF8String]];
610611
mysql_options(theConnection, MYSQL_SET_CHARSET_NAME, [encodingName UTF8String]);
611612

613+
my_bool cleartextAllowed = [self allowCleartextPlugin] ? TRUE : FALSE;
614+
mysql_options(theConnection, MYSQL_ENABLE_CLEARTEXT_PLUGIN, &cleartextAllowed);
615+
612616
// Set up the connection variables in the format MySQL needs, from the class-wide variables
613617
const char *theHost = NULL;
614618
const char *theUsername = "";
@@ -746,9 +750,11 @@ - (void)_mysqlConnection:(MYSQL *)connection wantsPassword:(void (^)(const char
746750
passwd = [delegate passwordForConnection:self authPlugin:plugin];
747751
}
748752

749-
// shortcut for empty/nil password
753+
// shortcut for nil/empty passwords
750754
if(![passwd length]) {
751-
inBlock(NULL);
755+
// nil means abort, "" is a valid password:
756+
// only invoke the block when the password is @"", otherwise we'll skip it which will make mysql abort the connection
757+
if(passwd) inBlock("");
752758
return;
753759
}
754760

Frameworks/SPMySQLFramework/Source/SPMySQLConnectionDelegate.h

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
* the secure store (Keychain), and the other connection details (user, host)
6969
* can be used to look it up and supplied on demand.
7070
*
71+
* NOTE: This will be called on the thread SPMySQL is running on (which *MAY* be a background thread)!
72+
*
7173
* @param connection The connection instance to supply the password for
7274
* @param pluginName The auth plugin libmysqlclients wants to use the password with
7375
*/

Interfaces/English.lproj/ConnectionView.xib

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="13F1507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
1+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="13F1911" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
33
<dependencies>
44
<deployment identifier="macosx"/>
55
<development version="5100" identifier="xcode"/>
@@ -23,6 +23,9 @@
2323
<outlet property="helpButton" destination="4829" id="5458"/>
2424
<outlet property="progressIndicator" destination="5422" id="5426"/>
2525
<outlet property="progressIndicatorText" destination="5423" id="5425"/>
26+
<outlet property="requestPasswordAccessoryView" destination="eXu-WW-1SA" id="67S-wG-21A"/>
27+
<outlet property="requestPasswordPasswordField" destination="iX8-1c-QGP" id="f2E-RB-7D1"/>
28+
<outlet property="requestPasswordPluginNameField" destination="qYD-Yv-BoC" id="4Ps-j5-3U9"/>
2629
<outlet property="saveFavoriteButton" destination="5869" id="5874"/>
2730
<outlet property="socketColorField" destination="5896" id="5897"/>
2831
<outlet property="socketConnectionFormContainer" destination="5162" id="5163"/>
@@ -2590,6 +2593,52 @@ DQ
25902593
</textField>
25912594
</subviews>
25922595
</customView>
2596+
<customView id="eXu-WW-1SA" userLabel="Request Password Accessory View">
2597+
<rect key="frame" x="-5" y="0.0" width="334" height="47"/>
2598+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2599+
<subviews>
2600+
<textField toolTip="The name of the authentication plugin MySQL wants to use" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="kJN-ff-ksF">
2601+
<rect key="frame" x="-2" y="30" width="105" height="17"/>
2602+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2603+
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Plugin:" id="r9R-qm-igR">
2604+
<font key="font" metaFont="system"/>
2605+
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
2606+
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
2607+
</textFieldCell>
2608+
</textField>
2609+
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="qYD-Yv-BoC">
2610+
<rect key="frame" x="107" y="30" width="229" height="17"/>
2611+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2612+
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="$pluginName" id="6ge-00-IRy">
2613+
<font key="font" metaFont="systemBold"/>
2614+
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
2615+
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
2616+
</textFieldCell>
2617+
</textField>
2618+
<secureTextField verticalHuggingPriority="750" id="iX8-1c-QGP">
2619+
<rect key="frame" x="109" y="0.0" width="225" height="22"/>
2620+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2621+
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="de0-rh-heM">
2622+
<font key="font" metaFont="system"/>
2623+
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
2624+
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
2625+
<allowedInputSourceLocales>
2626+
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
2627+
</allowedInputSourceLocales>
2628+
</secureTextFieldCell>
2629+
</secureTextField>
2630+
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="YdZ-UM-eof">
2631+
<rect key="frame" x="-2" y="2" width="105" height="17"/>
2632+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2633+
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Password:" id="afg-rd-MnW">
2634+
<font key="font" metaFont="system"/>
2635+
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
2636+
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
2637+
</textFieldCell>
2638+
</textField>
2639+
</subviews>
2640+
<point key="canvasLocation" x="104" y="923.5"/>
2641+
</customView>
25932642
</objects>
25942643
<resources>
25952644
<image name="button_action" width="30" height="22"/>

Source/SPConnectionController.h

+19
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
BOOL isConnecting;
7070
BOOL isEditingConnection;
7171
BOOL isTestingConnection;
72+
NSString *agreedInsecurePlugin;
73+
NSString *insecureOverridePassword;
7274

7375
// Standard details
7476
NSInteger previousType;
@@ -164,6 +166,10 @@
164166
IBOutlet NSMenuItem *favoritesSortByMenuItem;
165167
IBOutlet NSView *exportPanelAccessoryView;
166168
IBOutlet NSView *editButtonsView;
169+
170+
IBOutlet NSView *requestPasswordAccessoryView;
171+
IBOutlet NSTextField *requestPasswordPluginNameField;
172+
IBOutlet NSSecureTextField *requestPasswordPasswordField;
167173

168174
BOOL isEditingItemName;
169175
BOOL reverseFavoritesSort;
@@ -214,6 +220,18 @@
214220
@property (readwrite, retain) NSString *connectionSSHKeychainItemName;
215221
@property (readwrite, retain) NSString *connectionSSHKeychainItemAccount;
216222
@property (readwrite, assign) BOOL useCompression;
223+
/**
224+
* If the user was prompted to allow a connection with an insecure auth plugin,
225+
* the name of that plugin will be stored here (not persisted) so that we
226+
* don't have to ask again when duplicating a connection/reconnecting.
227+
*/
228+
@property (readwrite, copy, nonatomic) NSString *agreedInsecurePlugin;
229+
/**
230+
* If the user has given a password that is not the keychain password in
231+
* the insecure auth plugin request, we will store it in memory and keep the
232+
* other properties unchanged, since they are connected to GUI and/or backing stores
233+
*/
234+
@property (readwrite, copy, nonatomic) NSString *insecureOverridePassword;
217235

218236
#ifdef SP_CODA
219237
@property (readwrite, assign) SPDatabaseDocument *dbDocument;
@@ -224,6 +242,7 @@
224242

225243
- (NSString *)keychainPassword;
226244
- (NSString *)keychainPasswordForSSH;
245+
- (NSString *)actualPasswordForAuthPlugin:(NSString *)pluginName;
227246

228247
// Connection processes
229248
- (IBAction)initiateConnection:(id)sender;

Source/SPConnectionController.m

+146-7
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ - (void)_startEditingConnection;
114114

115115
- (void)_documentWillClose:(NSNotification *)notification;
116116

117+
- (void)_beginRequestPasswordForInsecurePlugin:(NSString *)pluginName;
118+
- (void)_insecurePasswordAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
119+
117120
static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, void *key);
118121
#endif
119122

@@ -163,6 +166,8 @@ @implementation SPConnectionController
163166
@synthesize sshKeyLocation;
164167
@synthesize sshPort;
165168
@synthesize useCompression;
169+
@synthesize agreedInsecurePlugin = agreedInsecurePlugin;
170+
@synthesize insecureOverridePassword = insecureOverridePassword;
166171

167172
#ifdef SP_CODA
168173
@synthesize dbDocument;
@@ -199,6 +204,133 @@ - (NSString *)keychainPasswordForSSH
199204
return kcSSHPassword;
200205
}
201206

207+
/**
208+
* This method is responsible for handing the user password to SPMySQL when it needs it.
209+
* It will receive the name of the auth plugin mysql plans to use and with
210+
* that it has to decide whether to fetch the password from keychain, or present
211+
* additional prompts to the user (e.g. in case the auth plugin is insecure).
212+
*
213+
* Note: The 5.5 libmysqlclient on the first attempt ignores the requested plugin
214+
* and tries to guess one instead. Only if that fails it will try to use
215+
* the server's suggested one. Thus this method may be invoked multiple times
216+
* during a single connection attempt.
217+
*
218+
* @return The actual user password to give to the mysql auth plugin.
219+
* This may be empty (no password).
220+
* This may be nil, which will be interpreted as "cancelled by user"
221+
*
222+
* This MUST be called on the UI thread!
223+
*/
224+
- (NSString *)actualPasswordForAuthPlugin:(NSString *)pluginName
225+
{
226+
NSArray *securePluginNames = @[
227+
@"mysql_native_password", //default on 4.1+
228+
//@"sha256_password", // not supported by the 5.5 client, only secure when used with TLS server cert checks!
229+
// over SSL: password is transmitted in plaintext
230+
// over plaintext: the mysql client uses assymetric crypto to encrypt the password and
231+
// send the encrypted plaintext to the server (this requires the OpenSSL
232+
// libs, so doesn't work with our client builds)
233+
];
234+
//INSECURE:
235+
// mysql_clear_password; Plaintext password
236+
// mysql_old_password; Deprecated, used with pre-4.1 servers. No longer supported in 5.7.5+ clients
237+
//UNSUPPORTED:
238+
// authentication_windows_client; Kerberos/NTLM auth. Not supported on UNIX by libmysqlclient
239+
// authentication_ldap_sasl_client; MySQL Enterprise
240+
// auth_test_plugin; Developer example only
241+
242+
// TODO incorporate SSL/TLS state in decision (cleartext over trusted SSL should be fine)
243+
// This is not possible right now, because someone with access to the Mac could just swap the SSL CA cert
244+
// and server ip to something he controls. So if Sequel Pro would just check that SSL was in use, it could
245+
// still be attacked to easily give the password away.
246+
// Instead we have to link all critical connection parameters (host+port+ssh-tunnel+ssl server or ca cert hash)
247+
// to the password in keychain and make sure none of them changed before giving the password to libmysqlclient.
248+
249+
if(![securePluginNames containsObject:pluginName]) {
250+
if(![pluginName isEqualToString:agreedInsecurePlugin]) {
251+
// since the user will probably take longer to answer the dialog than the connection timeout is
252+
// (and because the UI sheet is async anyway),
253+
// we will do a little trick and disconnect mysql right now and retry, once we actually have the password
254+
[self performSelector:@selector(_beginRequestPasswordForInsecurePlugin:) withObject:pluginName afterDelay:0.0];
255+
cancellingConnection = YES;
256+
return nil;
257+
}
258+
}
259+
260+
NSString *pass;
261+
// override password always wins
262+
if ((pass = [self insecureOverridePassword])) {
263+
;
264+
}
265+
// Only set the password if there is no Keychain item set and the connection is not being tested.
266+
// The connection will otherwise ask the delegate for passwords in the Keychain.
267+
else if ((!connectionKeychainItemName || isTestingConnection) && (pass = [self password])) {
268+
;
269+
}
270+
else {
271+
pass = [self keychainPassword];
272+
}
273+
274+
return (pass ? pass : @""); //returning nil would mean cancelled
275+
}
276+
277+
- (void)_beginRequestPasswordForInsecurePlugin:(NSString *)pluginName
278+
{
279+
// if the user presses "Disconnect" in the dialog OR
280+
// if this was only a test connection OR
281+
// if the connection failed anyway
282+
// -> agreedInsecurePlugin will be cleared again via -_restoreConnectionInterface
283+
[self setAgreedInsecurePlugin:pluginName];
284+
285+
//show modal warning dialog
286+
NSAlert *alert = [[NSAlert alloc] init]; //released in alert callback
287+
[alert setAlertStyle:NSAlertStyleCritical];
288+
[alert setMessageText:NSLocalizedString(@"Transmit password insecurely?",@"Connection dialog : password security error alert : title")];
289+
[alert setInformativeText:NSLocalizedString(@"The MySQL server has requested the password to be transmitted in an insecure manner. This could be indicative of an attack on the server or your network connection!\n\nIf you still want to continue anyway, manually reenter your connection password.",@"Connection dialog : password security error alert : detail message")];
290+
[alert setAccessoryView:requestPasswordAccessoryView];
291+
292+
[requestPasswordPluginNameField setStringValue:pluginName];
293+
294+
NSButton *connectAnywayButton = [alert addButtonWithTitle:NSLocalizedString(@"Connect Anyway",@"Connection dialog : password security error alert : confirm button")];
295+
[connectAnywayButton setTag:NSAlertDefaultReturn];
296+
297+
NSButton *disconnectButton = [alert addButtonWithTitle:NSLocalizedString(@"Disconnect",@"Connection dialog : password security error alert : cancel button")];
298+
[disconnectButton setTag:NSAlertAlternateReturn];
299+
300+
[alert beginSheetModalForWindow:[dbDocument parentWindow]
301+
modalDelegate:self
302+
didEndSelector:@selector(_insecurePasswordAlertDidEnd:returnCode:contextInfo:)
303+
contextInfo:NULL];
304+
}
305+
306+
- (void)_insecurePasswordAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
307+
{
308+
NSString *pass = nil;
309+
if(returnCode == NSAlertDefaultReturn) {
310+
pass = [NSString stringWithString:[requestPasswordPasswordField stringValue]];
311+
}
312+
[requestPasswordPasswordField setStringValue:@""];
313+
314+
[alert autorelease];
315+
316+
cancellingConnection = NO;
317+
318+
//cancelled by user?
319+
if(!pass) {
320+
[self _restoreConnectionInterface];
321+
return;
322+
}
323+
324+
// if the manually given password matches the keychain password there is no need to keep it in memory,
325+
// otherwise we use the override password ivar in order to not affect the normal logic
326+
if (!connectionKeychainItemName || ![pass isEqualToString:[self keychainPassword]]) {
327+
[self setInsecureOverridePassword:pass];
328+
}
329+
330+
[[pass retain] release]; //free it asap
331+
[self initiateMySQLConnection]; //reconnect
332+
}
333+
202334
#pragma mark -
203335
#pragma mark Connection processes
204336

@@ -765,6 +897,8 @@ - (void)updateFavoriteSelection:(id)sender
765897
if (connectionKeychainItemAccount) SPClear(connectionKeychainItemAccount);
766898
if (connectionSSHKeychainItemName) SPClear(connectionSSHKeychainItemName);
767899
if (connectionSSHKeychainItemAccount) SPClear(connectionSSHKeychainItemAccount);
900+
[self setAgreedInsecurePlugin:nil];
901+
[self setInsecureOverridePassword:nil];
768902

769903
SPTreeNode *node = [self selectedFavoriteNode];
770904
if ([node isGroup]) node = nil;
@@ -1764,7 +1898,11 @@ - (void)_restoreConnectionInterface
17641898
// Reset the window title
17651899
[dbDocument updateWindowTitle:self];
17661900
[[dbDocument parentTabViewItem] setLabel:[dbDocument displayName]];
1767-
1901+
1902+
//only store that if the connection attempt was successful
1903+
[self setAgreedInsecurePlugin:nil];
1904+
[self setInsecureOverridePassword:nil];
1905+
17681906
// Stop the current tab's progress indicator
17691907
[dbDocument setIsProcessing:NO];
17701908

@@ -2083,12 +2221,6 @@ - (void)initiateMySQLConnectionInBackground
20832221
}
20842222
}
20852223

2086-
// Only set the password if there is no Keychain item set and the connection is not being tested.
2087-
// The connection will otherwise ask the delegate for passwords in the Keychain.
2088-
if ((!connectionKeychainItemName || isTestingConnection) && [self password]) {
2089-
[mySQLConnection setPassword:[self password]];
2090-
}
2091-
20922224
// Enable SSL if set
20932225
if ([self useSSL]) {
20942226
[mySQLConnection setUseSSL:YES];
@@ -2130,6 +2262,11 @@ - (void)initiateMySQLConnectionInBackground
21302262
[mySQLConnection setUseKeepAlive:[[prefs objectForKey:SPUseKeepAlive] boolValue]];
21312263
[mySQLConnection setKeepAliveInterval:[[prefs objectForKey:SPKeepAliveInterval] floatValue]];
21322264

2265+
// We can always enable the cleartext plugin (this does not yet mean it will actually be used),
2266+
// since mysql will ask us for the password in -actualPasswordForAuthPlugin: at which point
2267+
// we get to decide what to do with the actual plugin.
2268+
[mySQLConnection setAllowCleartextPlugin:YES];
2269+
21332270
// Connect
21342271
[mySQLConnection connect];
21352272

@@ -3646,6 +3783,8 @@ - (void)dealloc
36463783
if (connectionKeychainItemAccount) SPClear(connectionKeychainItemAccount);
36473784
if (connectionSSHKeychainItemName) SPClear(connectionSSHKeychainItemName);
36483785
if (connectionSSHKeychainItemAccount) SPClear(connectionSSHKeychainItemAccount);
3786+
[self setAgreedInsecurePlugin:nil];
3787+
[self setInsecureOverridePassword:nil];
36493788

36503789
#ifndef SP_CODA
36513790
if (currentFavorite) SPClear(currentFavorite);

0 commit comments

Comments
 (0)