Skip to content

Commit 016e033

Browse files
committed
fix: SQL injection (backslash escape), orphaned credentials cleanup, stale columnDetails
1 parent 7f95df9 commit 016e033

5 files changed

Lines changed: 38 additions & 7 deletions

File tree

TableProMobile/TableProMobile/AppState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class AppState {
3030
sshProvider: sshProvider
3131
)
3232
connections = storage.load()
33+
secureStore.cleanOrphanedCredentials(validConnectionIds: Set(connections.map(\.id)))
3334

3435
syncCoordinator.onConnectionsChanged = { [weak self] merged in
3536
guard let self else { return }

TableProMobile/TableProMobile/Drivers/MySQLDriver.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ final class MySQLDriver: DatabaseDriver, @unchecked Sendable {
144144
}
145145

146146
func fetchForeignKeys(table: String, schema: String?) async throws -> [ForeignKeyInfo] {
147-
let safe = table.replacingOccurrences(of: "'", with: "''")
148-
let dbSafe = database.replacingOccurrences(of: "'", with: "''")
147+
let safe = SQLBuilder.escapeString(table)
148+
let dbSafe = SQLBuilder.escapeString(database)
149149
let query = """
150150
SELECT
151151
kcu.CONSTRAINT_NAME,

TableProMobile/TableProMobile/Helpers/SQLBuilder.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ enum SQLBuilder {
1919
}
2020

2121
static func escapeString(_ value: String) -> String {
22-
value.replacingOccurrences(of: "'", with: "''")
22+
value
23+
.replacingOccurrences(of: "\\", with: "\\\\")
24+
.replacingOccurrences(of: "'", with: "''")
2325
}
2426

2527
static func buildSelect(table: String, type: DatabaseType, limit: Int, offset: Int) -> String {

TableProMobile/TableProMobile/Platform/KeychainSecureStore.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ final class KeychainSecureStore: SecureStore {
3838
cachedAccessGroup = resolved
3939
return resolved
4040
}
41-
let fallback = "D7HJ5TFYCU.com.TablePro.shared"
41+
// Use non-shared access group as last resort — credentials won't sync
42+
// across devices but the app still functions
43+
let fallback = "com.TablePro.shared"
4244
cachedAccessGroup = fallback
4345
return fallback
4446
}
@@ -115,6 +117,34 @@ final class KeychainSecureStore: SecureStore {
115117
}
116118
}
117119

120+
/// Remove orphaned test connection credentials that may remain after a SIGKILL.
121+
/// Test credentials use temp UUIDs not associated with any saved connection.
122+
func cleanOrphanedCredentials(validConnectionIds: Set<UUID>) {
123+
let prefixes = ["com.TablePro.password.", "com.TablePro.sshpassword.", "com.TablePro.keypassphrase."]
124+
let query: [String: Any] = [
125+
kSecClass as String: kSecClassGenericPassword,
126+
kSecAttrService as String: serviceName,
127+
kSecAttrAccessGroup as String: accessGroup,
128+
kSecReturnAttributes as String: true,
129+
kSecMatchLimit as String: kSecMatchLimitAll,
130+
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
131+
kSecUseDataProtectionKeychain as String: true,
132+
]
133+
var result: AnyObject?
134+
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
135+
let items = result as? [[String: Any]] else { return }
136+
137+
for item in items {
138+
guard let account = item[kSecAttrAccount as String] as? String else { continue }
139+
for prefix in prefixes {
140+
guard account.hasPrefix(prefix) else { continue }
141+
let uuidString = String(account.dropFirst(prefix.count))
142+
guard let uuid = UUID(uuidString: uuidString),
143+
!validConnectionIds.contains(uuid) else { continue }
144+
try? delete(forKey: account)
145+
}
146+
}
147+
}
118148
}
119149

120150
enum KeychainError: Error, LocalizedError {

TableProMobile/TableProMobile/Views/DataBrowserView.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,7 @@ struct DataBrowserView: View {
238238

239239
// columnDetails (from fetchColumns) provides PK info for edit/delete.
240240
// columns (from query result) only have name/type, no PK metadata.
241-
if columnDetails.isEmpty {
242-
self.columnDetails = try await session.driver.fetchColumns(table: table.name, schema: nil)
243-
}
241+
self.columnDetails = try await session.driver.fetchColumns(table: table.name, schema: nil)
244242

245243
isLoading = false
246244
} catch {

0 commit comments

Comments
 (0)