Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 82 additions & 23 deletions apps/desktop/src-tauri/src/deeplink_actions.rs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impl TryFrom<&Url> for DeepLinkAction looks accidentally corrupted right now (multiple nested fn try_from(...) declarations + stray return Err(...) blocks). As-is it won’t compile.

I’d collapse it back to a single fn try_from(url: &Url) -> Result<...> body (keeping the macOS file:// early return) and then keep the scheme/host routing logic inside that one function.

Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,16 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
#[cfg(debug_assertions)]
PauseRecording,
#[cfg(debug_assertions)]
ResumeRecording,
#[cfg(debug_assertions)]
SwitchMicrophone {
mic_label: Option<String>,
},
OpenCamera {
camera: DeviceOrModelID,
},
#[cfg(debug_assertions)]
SetCameraPreviewState {
state: CameraPreviewState,
state: crate::camera::CameraPreviewState,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor CI/clippy thing: now that this uses crate::camera::CameraPreviewState directly, the #[cfg(debug_assertions)] use crate::camera::CameraPreviewState; import near the top becomes unused in cargo clippy -- -D warnings (debug profile). Might be worth deleting that import (or un-gating + using it).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the enum uses crate::camera::CameraPreviewState directly, the #[cfg(debug_assertions)] use crate::camera::CameraPreviewState; import at the top becomes unused; worth removing if this crate denies warnings in CI.

},
OpenEditor {
project_path: PathBuf,
Expand Down Expand Up @@ -164,21 +163,51 @@ impl TryFrom<&Url> for DeepLinkAction {
.map_err(|_| ActionParseFromUrlError::Invalid);
}

match url.domain() {
Some("action") => {}
Some(_) => return Err(ActionParseFromUrlError::NotAction),
None => return Err(ActionParseFromUrlError::Invalid),
if !matches!(url.scheme(), "cap" | "cap-desktop") {
Comment thread
BMNTR marked this conversation as resolved.
Comment thread
BMNTR marked this conversation as resolved.
return Err(ActionParseFromUrlError::NotAction);
}

let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let json_value = params
.get("value")
.ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
Ok(action)
match url.host_str() {
Comment thread
BMNTR marked this conversation as resolved.
Some("action") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let json_value = params
.get("value")
.ok_or(ActionParseFromUrlError::Invalid)?;
let action: Self = serde_json::from_str(json_value)
.map_err(|e| ActionParseFromUrlError::ParseFailed(e.to_string()))?;
Ok(action)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Sensitive recording controls exposed in production via unauthenticated deep links

Sensitive recording controls now exposed via unauthenticated deep links in production builds.

Add authorization checks or user confirmation before executing sensitive controls via deep links.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="apps/desktop/src-tauri/src/deeplink_actions.rs">
<violation number="1" location="apps/desktop/src-tauri/src/deeplink_actions.rs:177">
<priority>P1</priority>
<title>Sensitive recording controls exposed in production via unauthenticated deep links</title>
<evidence>The PR removes #[cfg(debug_assertions)] guards from PauseRecording, ResumeRecording, OpenCamera, and SetCameraPreviewState, and adds direct URL path handlers (cap://pause, cap://resume, cap://switch-mic, cap://switch-camera) that allow any application or webpage to trigger these sensitive controls without authentication. The original debug guards indicate these actions were intentionally restricted from production builds.</evidence>
<recommendation>Add an authorization check before executing sensitive recording controls via deep links, such as requiring user confirmation or a signed/validated token in the URL. Alternatively, gate these actions behind debug assertions or a developer mode toggle unless there is explicit user consent for each invocation.</recommendation>
</violation>
</file>

Some("stop" | "stop-recording") => Ok(Self::StopRecording),
Some("pause" | "pause-recording") => Ok(Self::PauseRecording),
Some("resume" | "resume-recording") => Ok(Self::ResumeRecording),
Some("switch-mic" | "switch-microphone") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let mic_label = params
Comment thread
BMNTR marked this conversation as resolved.
.get("mic_label")
.or_else(|| params.get("label"))
.and_then(|s| (!s.is_empty()).then(|| s.to_string()));
Ok(Self::SwitchMicrophone { mic_label })
}
Some("open-camera" | "switch-camera") => {
let params = url
.query_pairs()
.collect::<std::collections::HashMap<_, _>>();
let device_id = params
Comment thread
BMNTR marked this conversation as resolved.
.get("id")
.or_else(|| params.get("camera"))
.and_then(|s| (!s.is_empty()).then(|| s.to_string()))
.ok_or(ActionParseFromUrlError::Invalid)?;
Ok(Self::OpenCamera {
camera: DeviceOrModelID::DeviceID(device_id),
})
}
Some(_) => Err(ActionParseFromUrlError::NotAction),
None => Err(ActionParseFromUrlError::Invalid),
}
Comment thread
BMNTR marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -244,15 +273,18 @@ impl DeepLinkAction {
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::PauseRecording => {
crate::recording::pause_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::ResumeRecording => {
crate::recording::resume_recording(app.clone(), app.state()).await
}
#[cfg(debug_assertions)]
DeepLinkAction::SwitchMicrophone { mic_label } => {
let state = app.state::<ArcLock<App>>();
crate::set_mic_input(state, mic_label)
.await
.map_err(|e| e.to_string())
}
DeepLinkAction::OpenCamera { camera } => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this arm is no longer #[cfg(debug_assertions)], double-check release builds still compile: app.emit(...) is provided by tauri::Emitter, but the use tauri::Emitter; import above is still debug-only. Easiest fix is to un-gate that import or switch to UFCS (tauri::Emitter::emit(app, ...)) so the trait doesn’t need to be in scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since OpenCamera is now reachable in release builds, note this arm calls app.emit(...) further down. That requires the tauri::Emitter trait to be in scope, but the import at the top is still #[cfg(debug_assertions)] — release builds will fail unless you un-gate that import (or switch to tauri::Emitter::emit(app, ...)).

crate::set_camera_input(
app.clone(),
Expand Down Expand Up @@ -282,7 +314,6 @@ impl DeepLinkAction {

Ok(())
}
#[cfg(debug_assertions)]
DeepLinkAction::SetCameraPreviewState { state } => {
crate::set_camera_preview_state(app.state(), state).await
}
Expand Down Expand Up @@ -358,7 +389,6 @@ mod tests {
);
}

#[cfg(debug_assertions)]
#[test]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the new public contract is the direct host routes (e.g. cap://pause, cap://switch-mic?label=..., cap://switch-camera?id=...), it’d be nice to add a couple small tests for those alongside the existing cap-desktop://action?... coverage.

fn parses_pause_and_resume_action_urls() {
let pause_url = Url::parse("cap-desktop://action?value=%22pause_recording%22").unwrap();
Expand All @@ -374,6 +404,35 @@ mod tests {
);
}

#[test]
fn parses_direct_scheme_urls() {
let pause_url = Url::parse("cap://pause").unwrap();
let resume_url = Url::parse("cap://resume").unwrap();
let mic_url = Url::parse("cap://switch-mic?label=Shure%20MV7%2B").unwrap();
let camera_url = Url::parse("cap://switch-camera?id=cam-1").unwrap();

assert_eq!(
DeepLinkAction::try_from(&pause_url),
Ok(DeepLinkAction::PauseRecording)
);
assert_eq!(
DeepLinkAction::try_from(&resume_url),
Ok(DeepLinkAction::ResumeRecording)
);
assert_eq!(
DeepLinkAction::try_from(&mic_url),
Ok(DeepLinkAction::SwitchMicrophone {
mic_label: Some("Shure MV7+".to_string())
})
);
assert_eq!(
DeepLinkAction::try_from(&camera_url),
Ok(DeepLinkAction::OpenCamera {
camera: DeviceOrModelID::DeviceID("cam-1".to_string())
})
);
}

#[cfg(debug_assertions)]
#[test]
fn parses_area_recording_action_url() {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"updater": { "active": false, "pubkey": "" },
"deep-link": {
"desktop": {
"schemes": ["cap-desktop"]
"schemes": ["cap-desktop", "cap"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just sanity-checking production: this repo also has tauri.prod.conf.json. If release builds use that file instead of merging with tauri.conf.json, the new cap scheme won’t be registered in production and cap://... links still won’t route. Might be worth either confirming config merge behavior in the build pipeline, or duplicating the deep-link plugin config into the prod config.

}
}
},
Expand Down