fix(export): add obsidian:// to allowed protocol whitelist (#14366)

<!-- Template from
https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft:
https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->

<!--

🚨 Branch Strategy Change (Effective April 3, 2026) 🚨

The `main` branch is now under CODE FREEZE.

- main branch: Only accepts critical bug fixes via `hotfix/*` branches.
Fix PRs must be minimal in scope and must not include any refactoring
code.
- v2 branch: All new features, refactoring, and optimizations should be
submitted to the `v2` branch.

If you are submitting a bug fix to main, please ensure your PR is from a
`hotfix/*` branch.

-->

### What this PR does

Before this PR:

The "Export to Obsidian" feature reports "Export success" but no
document is actually created in Obsidian. The `obsidian://new` URI is
silently blocked by the protocol whitelist in `security.ts` and
`preload/index.ts`, which only allows `http:`, `https:`, and `mailto:`
schemes.

After this PR:

The `obsidian:` protocol is added to the allowed protocol whitelist in
both `security.ts` and `preload/index.ts`, so the `obsidian://new` URI
can be correctly forwarded to the system protocol handler and open
Obsidian.

<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)`
format, will close the issue(s) when PR gets merged)*: -->

Fixes #

### Why we need it and why it was done in this way

The export flow works by:
1. Writing markdown content to the clipboard
(`navigator.clipboard.writeText`)
2. Opening `obsidian://new?file=...&vault=...&clipboard` via
`window.open()`
3. Electron's `setWindowOpenHandler` intercepts the call and checks
`isSafeExternalUrl()`
4. `isSafeExternalUrl()` rejects `obsidian:` protocol → request is
silently denied
5. Frontend has no failure detection and reports success

The log confirms this with: `Blocked shell.openExternal for untrusted
URL scheme: obsidian://new?file=...`

The following tradeoffs were made:

- Only `obsidian:` is added to the whitelist, keeping the security
policy minimal. Unlike arbitrary custom protocols (e.g. `ms-msdt:`,
`calculator:`), `obsidian:` is a well-known note-taking app protocol
that Cherry Studio explicitly integrates with.

The following alternatives were considered:

- Writing files directly to the vault path via `fs.writeFile` in the
main process, bypassing the URI scheme entirely. Rejected because it
would be a larger change and the current URI-based approach is the
intended design.

Links to places where the discussion took place: <!-- optional: slack,
other GH issue, mailinglist, ... -->

### Breaking changes

None.

<!-- optional -->

### Special notes for your reviewer
- Reproduced on Windows with Cherry Studio v1.9.1. The bug is 100%
reproducible on all platforms.
- Verified fix with `pnpm dev` on Windows — Obsidian export works
correctly after the change.
- Unit test passes: `vitest run --project main
src/main/services/__tests__/security.test.ts` (8 tests passed).
<!-- optional -->

### Checklist

This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [x] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR
requires additional action from users switching to the new release,
include the string "action required".
2. If no release note is required, just write "NONE".
3. Only include user-facing changes (new features, bug fixes visible to
users, UI changes, behavior changes). For CI, maintenance, internal
refactoring, build tooling, or other non-user-facing work, write "NONE".
-->

```release-note
fix: Obsidian export no longer silently fails — obsidian:// protocol is now allowed through the security whitelist
```

Signed-off-by: rfslzy <604364013@qq.com>
Co-authored-by: SuYao <sy20010504@gmail.com>
This commit is contained in:
rfslzy
2026-04-18 21:23:54 +08:00
committed by GitHub
parent d89ce08716
commit 76ec2f1c0e
3 changed files with 6 additions and 2 deletions

View File

@@ -17,6 +17,10 @@ describe('isSafeExternalUrl', () => {
expect(isSafeExternalUrl('mailto:user@example.com')).toBe(true)
})
it('allows obsidian:// protocol', () => {
expect(isSafeExternalUrl('obsidian://new?file=test&vault=myvault&clipboard')).toBe(true)
})
it('rejects file:// protocol', () => {
expect(isSafeExternalUrl('file:///etc/passwd')).toBe(false)
expect(isSafeExternalUrl('file://localhost/tmp')).toBe(false)

View File

@@ -2,7 +2,7 @@
* Security utility functions for the main process.
*/
const ALLOWED_EXTERNAL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:'])
const ALLOWED_EXTERNAL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'obsidian:'])
/**
* Check whether a URL is safe to open via shell.openExternal().

View File

@@ -434,7 +434,7 @@ const api = {
shell: {
openExternal: (url: string, options?: Electron.OpenExternalOptions) => {
// Defense-in-depth: validate URL scheme before forwarding to shell.openExternal
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:']
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:', 'obsidian:']
try {
const parsed = new URL(url)
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {