Add input validation to prevent command injection
Security improvements: - Language selection now uses dropdown with 31 supported languages - Model path validated: must be .bin file, no path traversal - Validation runs before transcription execution - Invalid inputs show error status instead of executing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,51 @@ struct Defaults {
|
|||||||
static let playSounds = "playSounds"
|
static let playSounds = "playSounds"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Supported Languages (Whisper)
|
||||||
|
struct SupportedLanguages {
|
||||||
|
static let codes: [String: String] = [
|
||||||
|
"hu": "Magyar",
|
||||||
|
"en": "English",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"fr": "Français",
|
||||||
|
"es": "Español",
|
||||||
|
"it": "Italiano",
|
||||||
|
"pt": "Português",
|
||||||
|
"nl": "Nederlands",
|
||||||
|
"pl": "Polski",
|
||||||
|
"ru": "Русский",
|
||||||
|
"uk": "Українська",
|
||||||
|
"cs": "Čeština",
|
||||||
|
"sk": "Slovenčina",
|
||||||
|
"ro": "Română",
|
||||||
|
"hr": "Hrvatski",
|
||||||
|
"sr": "Srpski",
|
||||||
|
"sl": "Slovenščina",
|
||||||
|
"ja": "日本語",
|
||||||
|
"zh": "中文",
|
||||||
|
"ko": "한국어",
|
||||||
|
"ar": "العربية",
|
||||||
|
"tr": "Türkçe",
|
||||||
|
"vi": "Tiếng Việt",
|
||||||
|
"th": "ไทย",
|
||||||
|
"el": "Ελληνικά",
|
||||||
|
"he": "עברית",
|
||||||
|
"hi": "हिन्दी",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"da": "Dansk",
|
||||||
|
"fi": "Suomi",
|
||||||
|
"no": "Norsk"
|
||||||
|
]
|
||||||
|
|
||||||
|
static func isValid(_ code: String) -> Bool {
|
||||||
|
return codes.keys.contains(code.lowercased())
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sortedCodes: [(code: String, name: String)] {
|
||||||
|
return codes.sorted { $0.value < $1.value }.map { (code: $0.key, name: $0.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - App Delegate
|
// MARK: - App Delegate
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
var statusItem: NSStatusItem!
|
var statusItem: NSStatusItem!
|
||||||
@@ -98,18 +143,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
langLabel.frame = NSRect(x: 20, y: y, width: labelWidth, height: 24)
|
langLabel.frame = NSRect(x: 20, y: y, width: labelWidth, height: 24)
|
||||||
contentView.addSubview(langLabel)
|
contentView.addSubview(langLabel)
|
||||||
|
|
||||||
let langField = NSTextField(string: language)
|
let langPopup = NSPopUpButton(frame: NSRect(x: controlX, y: y, width: 180, height: 24), pullsDown: false)
|
||||||
langField.frame = NSRect(x: controlX, y: y, width: 60, height: 24)
|
langPopup.tag = 1
|
||||||
langField.tag = 1
|
for lang in SupportedLanguages.sortedCodes {
|
||||||
langField.target = self
|
langPopup.addItem(withTitle: "\(lang.name) (\(lang.code))")
|
||||||
langField.action = #selector(languageChanged(_:))
|
langPopup.lastItem?.representedObject = lang.code
|
||||||
contentView.addSubview(langField)
|
}
|
||||||
|
// Select current language
|
||||||
let langHint = NSTextField(labelWithString: "(hu, en, de, fr, es...)")
|
if let index = SupportedLanguages.sortedCodes.firstIndex(where: { $0.code == language }) {
|
||||||
langHint.frame = NSRect(x: 210, y: y, width: 150, height: 24)
|
langPopup.selectItem(at: index)
|
||||||
langHint.textColor = .secondaryLabelColor
|
}
|
||||||
langHint.font = NSFont.systemFont(ofSize: 11)
|
langPopup.target = self
|
||||||
contentView.addSubview(langHint)
|
langPopup.action = #selector(languageChanged(_:))
|
||||||
|
contentView.addSubview(langPopup)
|
||||||
|
|
||||||
y -= 40
|
y -= 40
|
||||||
|
|
||||||
@@ -167,13 +213,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func languageChanged(_ sender: NSTextField) {
|
@objc func languageChanged(_ sender: NSPopUpButton) {
|
||||||
language = sender.stringValue
|
if let code = sender.selectedItem?.representedObject as? String {
|
||||||
|
language = code
|
||||||
NSLog("Language changed to: \(language)")
|
NSLog("Language changed to: \(language)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func modelPathChanged(_ sender: NSTextField) {
|
@objc func modelPathChanged(_ sender: NSTextField) {
|
||||||
modelPath = sender.stringValue
|
let newPath = sender.stringValue
|
||||||
|
let validation = isValidModelPath(newPath)
|
||||||
|
if validation.valid {
|
||||||
|
modelPath = newPath
|
||||||
|
}
|
||||||
checkModelExists()
|
checkModelExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,10 +278,31 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Model Check
|
// MARK: - Model Validation
|
||||||
|
func isValidModelPath(_ path: String) -> (valid: Bool, error: String?) {
|
||||||
|
// Check extension
|
||||||
|
if !path.lowercased().hasSuffix(".bin") {
|
||||||
|
return (false, "Model must be a .bin file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
let normalized = (path as NSString).standardizingPath
|
||||||
|
if normalized.contains("..") {
|
||||||
|
return (false, "Invalid path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file exists
|
||||||
|
if !FileManager.default.fileExists(atPath: normalized) {
|
||||||
|
return (false, "Model not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func checkModelExists() {
|
func checkModelExists() {
|
||||||
if !FileManager.default.fileExists(atPath: modelPath) {
|
let validation = isValidModelPath(modelPath)
|
||||||
updateStatus("⚠️ Model not found")
|
if !validation.valid {
|
||||||
|
updateStatus("⚠️ \(validation.error ?? "Invalid model")")
|
||||||
} else {
|
} else {
|
||||||
updateStatus("Ready")
|
updateStatus("Ready")
|
||||||
}
|
}
|
||||||
@@ -359,9 +432,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
// MARK: - Transcription
|
// MARK: - Transcription
|
||||||
func transcribe() {
|
func transcribe() {
|
||||||
|
// Validate inputs before execution
|
||||||
|
let modelValidation = isValidModelPath(modelPath)
|
||||||
|
guard modelValidation.valid else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusItem.button?.title = "🎤"
|
||||||
|
self.updateStatus("⚠️ \(modelValidation.error ?? "Invalid model")")
|
||||||
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard SupportedLanguages.isValid(language) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusItem.button?.title = "🎤"
|
||||||
|
self.updateStatus("⚠️ Invalid language")
|
||||||
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/whisper-cli")
|
task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/whisper-cli")
|
||||||
task.arguments = ["-m", modelPath, "-l", language, "-f", audioFilePath]
|
task.arguments = ["-m", modelPath, "-l", language.lowercased(), "-f", audioFilePath]
|
||||||
|
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
task.standardOutput = pipe
|
task.standardOutput = pipe
|
||||||
|
|||||||
Reference in New Issue
Block a user