Skip to content
Merged
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
20 changes: 19 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ ipcMain.on('open-settings', () => {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
webSecurity: false,
webSecurity: true,
},
})

Expand All @@ -614,6 +614,24 @@ ipcMain.on('open-settings', () => {
})
})


ipcMain.handle('openai-chat', async (_, { model, messages, apiKey, baseURL }) => {
try {
const OpenAI = (await import('openai')).default
const openai = new OpenAI({
apiKey: apiKey || 'dummy',
baseURL: baseURL || undefined,
})
Comment on lines +621 to +624

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect OpenAI IPC call sites and provider/baseURL sources.
# Expectation: `baseURL` should come from a trusted provider selection or validated settings, not arbitrary renderer text.
rg -n -C4 --type=ts --type=tsx '\bopenAIChat\s*\(|\bbaseURL\b|openrouter|api\.openai|openai-chat' src electron

Repository: VariableThe/PaperCache

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Find and inspect the openai-chat IPC handler and baseURL usage
rg -n 'openai-chat|baseURL' --type-list | head -20

Repository: VariableThe/PaperCache

Length of output: 456


🏁 Script executed:

#!/bin/bash
# List all TypeScript files in the electron directory to understand structure
find electron src -name "*.ts" -o -name "*.js" | head -20

Repository: VariableThe/PaperCache

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Search for openai-chat IPC handler definition
rg -n 'openai-chat' --color=never -A 10 -B 2

Repository: VariableThe/PaperCache

Length of output: 1909


🏁 Script executed:

#!/bin/bash
# Search for baseURL references to understand how it's passed
rg -n 'baseURL' --color=never -B 3 -A 3

Repository: VariableThe/PaperCache

Length of output: 2313


🏁 Script executed:

#!/bin/bash
# Inspect App.tsx around the openAIChat call to understand finalBaseUrl source
cat -n src/App.tsx | sed -n '300,340p'

Repository: VariableThe/PaperCache

Length of output: 2374


🏁 Script executed:

#!/bin/bash
# Search for where finalBaseUrl is defined
rg -n 'finalBaseUrl' --color=never -B 5 -A 2

Repository: VariableThe/PaperCache

Length of output: 1404


🏁 Script executed:

#!/bin/bash
# Search for baseURL source in settings or provider configuration
rg -n 'baseURL|baseUrl|base_url' --color=never -B 2 -A 2 src/

Repository: VariableThe/PaperCache

Length of output: 656


🏁 Script executed:

#!/bin/bash
# Find where apiBaseUrl is defined in App.tsx
rg -n 'apiBaseUrl' --color=never -B 5 -A 2 src/App.tsx

Repository: VariableThe/PaperCache

Length of output: 1334


Validate the renderer-supplied baseURL before creating the OpenAI client.

The baseURL at line 623 is sourced from user settings in localStorage (papercache-api-base-url) and passed directly to the OpenAI client constructor without validation. This allows an attacker to redirect API requests to arbitrary servers, leaking the API key to attacker-controlled endpoints. Add an allowlist of trusted provider origins (e.g., https://api.openai.com/v1, https://openrouter.ai/api/v1) and validate the URL against it before use.

🛡️ Suggested allowlist guard
+const ALLOWED_OPENAI_BASE_URLS = new Set([
+  'https://api.openai.com/v1',
+  'https://openrouter.ai/api/v1',
+])
+
+function normalizeOpenAIBaseURL(baseURL?: string) {
+  if (!baseURL) return undefined
+
+  const url = new URL(baseURL)
+  url.hash = ''
+  url.search = ''
+  const normalized = url.toString().replace(/\/$/, '')
+
+  if (!ALLOWED_OPENAI_BASE_URLS.has(normalized)) {
+    throw new Error('Unsupported OpenAI base URL')
+  }
+
+  return normalized
+}
+
 ipcMain.handle('openai-chat', async (_, { model, messages, apiKey, baseURL }) => {
   try {
     const OpenAI = (await import('openai')).default
     const openai = new OpenAI({
       apiKey: apiKey || 'dummy',
-      baseURL: baseURL || undefined,
+      baseURL: normalizeOpenAIBaseURL(baseURL),
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/main.ts` around lines 621 - 624, The baseURL parameter passed to the
OpenAI client constructor is sourced from localStorage without validation,
creating a security vulnerability where malicious URLs could redirect API
requests and leak credentials. Create a whitelist constant containing trusted
provider origins such as https://api.openai.com/v1 and
https://openrouter.ai/api/v1, then validate the baseURL against this allowlist
before passing it to the OpenAI constructor at line 623. If the baseURL is not
in the allowlist or is invalid, use undefined instead to fall back to the
default endpoint.

const completion = await openai.chat.completions.create({
model: model,
messages: messages,
})
return completion
} catch (error: any) {
throw new Error(error.message || 'Unknown API Error')
Comment on lines +617 to +631

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect declared TS/Node/Electron targets for ErrorOptions support.
fd -i '^(package\.json|tsconfig.*)$' -x sh -c 'echo "===== $1"; sed -n "1,220p" "$1"' sh {}

Repository: VariableThe/PaperCache

Length of output: 4605


🏁 Script executed:

# Locate electron/main.ts file
find . -name "main.ts" -type f | head -20

Repository: VariableThe/PaperCache

Length of output: 85


🏁 Script executed:

# Check the directory structure
ls -la electron/ 2>/dev/null || echo "No electron directory found in root"
find . -path "*/electron/main.ts" -type f

Repository: VariableThe/PaperCache

Length of output: 322


🏁 Script executed:

# Check if there's a separate tsconfig for electron
fd -i '^tsconfig.*\.json$' | xargs cat | grep -A 5 -B 5 "electron" 2>/dev/null || echo "No electron-specific tsconfig found"

Repository: VariableThe/PaperCache

Length of output: 102


🏁 Script executed:

# Read lines around 617-631 to verify the code
sed -n '610,640p' ./electron/main.ts

Repository: VariableThe/PaperCache

Length of output: 774


🏁 Script executed:

# Check file size and line count
wc -l ./electron/main.ts

Repository: VariableThe/PaperCache

Length of output: 89


🏁 Script executed:

# Check vite.config.ts for electron configuration
cat vite.config.ts 2>/dev/null || echo "Not in root"
find . -name "vite.config.ts" -type f -exec cat {} \;

Repository: VariableThe/PaperCache

Length of output: 1658


🏁 Script executed:

# Check package.json for build process and electron handling
grep -A 5 '"build"' ./package.json

Repository: VariableThe/PaperCache

Length of output: 457


🏁 Script executed:

# Check ESLint configuration
find . -name ".eslintrc*" -o -name "eslint.config.*" | xargs cat 2>/dev/null

Repository: VariableThe/PaperCache

Length of output: 8847


🏁 Script executed:

# Verify ErrorOptions is supported by checking if TypeScript 6.0.2 supports Error constructor with cause
python3 << 'PY'
# ES2022 and later support Error constructor with { cause } option
# TypeScript 4.6+ (2022) supports this
# The project uses TypeScript 6.0.2 and target ES2023, so ErrorOptions is fully supported

ts_version = "6.0.2"
target = "es2023"
error_options_support = True  # Available since TS 4.6 and ES2022

print(f"TypeScript version: {ts_version}")
print(f"Target: {target}")
print(f"ErrorOptions ({ cause: ... }) support: {error_options_support}")
print()
print("Analysis:")
print("✓ ES2023 target supports Error constructor with cause parameter")
print("✓ TypeScript 6.0.2 supports ErrorOptions type definition")
print("✓ The proposed fix is valid and will work without fallback")
PY

Repository: VariableThe/PaperCache

Length of output: 262


Preserve the caught SDK error when rethrowing.

The current code at lines 617-631 loses error context by catching error: any and rethrowing only its message. Change the catch block to use unknown type, safely derive the message, and attach the original error as cause:

Proposed fix
 ipcMain.handle('openai-chat', async (_, { model, messages, apiKey, baseURL }) => {
   try {
     const OpenAI = (await import('openai')).default
     const openai = new OpenAI({
       apiKey: apiKey || 'dummy',
       baseURL: baseURL || undefined,
     })
     const completion = await openai.chat.completions.create({
       model: model,
       messages: messages,
     })
     return completion
-  } catch (error: any) {
-    throw new Error(error.message || 'Unknown API Error')
+  } catch (error: unknown) {
+    const message = error instanceof Error ? error.message : 'Unknown API Error'
+    throw new Error(message, { cause: error })
   }
 })

Your project targets ES2023 with TypeScript 6.0.2, both of which fully support ErrorOptions with the cause parameter—no fallback needed.

🧰 Tools
🪛 ESLint

[error] 617-618: Delete

(prettier/prettier)


[error] 631-631: There is no cause attached to the symptom error being thrown.

(preserve-caught-error)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/main.ts` around lines 617 - 631, In the IPC handler for
'openai-chat', the catch block currently loses error context by catching with
`error: any` and rethrowing only the message. Change the catch parameter type
from `error: any` to `error: unknown`, then safely extract the message property
from the unknown error type, and rethrow a new Error with the original error
attached via the `cause` option in the Error constructor to preserve the full
error context for debugging.

Source: Linters/SAST tools

}
})

ipcMain.on('quit-app', () => {
app.quit()
})
Expand Down
1 change: 1 addition & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveNote: (id: string, content: string) => ipcRenderer.invoke('save-note', { id, content }),
deleteNote: (id: string) => ipcRenderer.invoke('delete-note', id),
renameNote: (oldId: string, newId: string) => ipcRenderer.invoke('rename-note', { oldId, newId }),
openAIChat: (args: { model: string, messages: { role: string; content: string }[], apiKey: string, baseURL: string }) => ipcRenderer.invoke('openai-chat', args),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap the openAIChat payload type so lint passes.

The one-line object type is currently flagged by prettier/prettier.

🧹 Proposed formatting fix
-  openAIChat: (args: { model: string, messages: { role: string; content: string }[], apiKey: string, baseURL: string }) => ipcRenderer.invoke('openai-chat', args),
+  openAIChat: (args: {
+    model: string
+    messages: { role: string; content: string }[]
+    apiKey: string
+    baseURL: string
+  }) => ipcRenderer.invoke('openai-chat', args),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
openAIChat: (args: { model: string, messages: { role: string; content: string }[], apiKey: string, baseURL: string }) => ipcRenderer.invoke('openai-chat', args),
openAIChat: (args: {
model: string
messages: { role: string; content: string }[]
apiKey: string
baseURL: string
}) => ipcRenderer.invoke('openai-chat', args),
🧰 Tools
🪛 ESLint

[error] 9-9: Replace ·model:·string,·messages:·{·role:·string;·content:·string·}[],·apiKey:·string,·baseURL:·string with ⏎····model:·string⏎····messages:·{·role:·string;·content:·string·}[]⏎····apiKey:·string⏎····baseURL:·string⏎·

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/preload.ts` at line 9, The openAIChat function definition has an
inline object type that exceeds the line length limit set by prettier. Refactor
the args parameter type definition by breaking it across multiple lines: move
the object type properties (model, messages, apiKey, baseURL) each to their own
line and properly format the nested messages type to comply with prettier
formatting standards.

Source: Linters/SAST tools

readNote: (id: string) => ipcRenderer.invoke('read-note', id),
exportNote: (filename: string, content: string) =>
ipcRenderer.invoke('export-note', filename, content),
Expand Down
Loading
Loading