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
17 changes: 17 additions & 0 deletions electron/ipc/project/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ describe("local media path policy", () => {
await expect(isAllowedLocalMediaPath(pendingExportPath)).resolves.toBe(true);
});

it("allows media files in the configured recordings directory", async () => {
const configuredRecordingsDir = path.join(tempRoot, "Configured recordings");
const recordingPath = path.join(configuredRecordingsDir, "recording-123.mic.wav");
await fs.mkdir(configuredRecordingsDir, { recursive: true });
await fs.writeFile(recordingPath, "audio");
await fs.writeFile(
path.join(userDataPath, "recordings-settings.json"),
JSON.stringify({ recordingsDir: configuredRecordingsDir }),
);

const { resolveApprovedLocalMediaPath } = await import("./manager");

await expect(resolveApprovedLocalMediaPath(recordingPath)).resolves.toBe(
await fs.realpath(recordingPath),
);
});

it("approves media-server access for approved external files resolved through the URL policy", async () => {
const downloadsPath = path.join(tempRoot, "Downloads");
const videoPath = path.join(downloadsPath, "external-video.mp4");
Expand Down
12 changes: 12 additions & 0 deletions electron/ipc/project/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export function isAllowedLocalReadPath(candidatePath: string) {
// become fetchable inside the app.
export async function isAllowedLocalMediaPath(candidatePath: string) {
const normalizedCandidatePath = normalizePath(candidatePath);
// The recordings location can be changed in settings, so the compile-time
// default RECORDINGS_DIR alone is not sufficient for media created there.
// Canonicalize the configured root as well as the candidate to retain the
// symlink protection enforced by isAllowedLocalReadPath.
const recordingsDir = await getRecordingsDir();
const canonicalRecordingsDir = await fs
.realpath(recordingsDir)
.catch(() => normalizePath(recordingsDir));
if (isPathInsideDirectory(normalizedCandidatePath, canonicalRecordingsDir)) {
return true;
Comment on lines +103 to +112

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant files and nearby context.
git ls-files 'electron/ipc/project/*' 'electron/**/*manager*' 'electron/**/*media*' | sed -n '1,120p'

printf '\n--- manager.ts outline ---\n'
ast-grep outline electron/ipc/project/manager.ts --view expanded || true

printf '\n--- manager.ts relevant lines ---\n'
sed -n '1,220p' electron/ipc/project/manager.ts

printf '\n--- search for helper definitions/usages ---\n'
rg -n "isAllowedLocalMediaPath|isAllowedLocalReadPath|resolveApprovedLocalMediaPath|getRecordingsDir|isPathInsideDirectory|normalizePath" electron -S

Repository: webadderallorg/Recordly

Length of output: 18310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' electron/ipc/project/manager.ts
printf '\n---\n'
rg -n "isAllowedLocalMediaPath|isAllowedLocalReadPath|resolveApprovedLocalMediaPath" electron -S

Repository: webadderallorg/Recordly

Length of output: 11154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- manager.test.ts symlink-related cases ---'
sed -n '1,220p' electron/ipc/project/manager.test.ts

printf '\n%s\n' '--- filesystem probe for the fast-path behavior ---'
node - <<'JS'
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');

function normalizePath(p) {
  return path.normalize(p);
}
function isPathInsideDirectory(candidatePath, directoryPath) {
  const normalizedCandidatePath = normalizePath(candidatePath);
  const normalizedDirectoryPath = normalizePath(directoryPath);
  return (
    normalizedCandidatePath === normalizedDirectoryPath ||
    normalizedCandidatePath.startsWith(`${normalizedDirectoryPath}${path.sep}`)
  );
}

const base = fs.mkdtempSync(path.join(os.tmpdir(), 'recordly-probe-'));
const recordings = path.join(base, 'recordings');
const outside = path.join(base, 'outside');
fs.mkdirSync(recordings);
fs.mkdirSync(outside);
const target = path.join(outside, 'track.wav');
fs.writeFileSync(target, 'x');
const symlink = path.join(recordings, 'track.wav');
fs.symlinkSync(target, symlink);

const canonicalRecordingsDir = fs.realpathSync(recordings);
const normalizedCandidatePath = normalizePath(symlink);
const candidateReal = fs.realpathSync(normalizedCandidatePath);

console.log(JSON.stringify({
  recordings,
  outside,
  symlink,
  target,
  canonicalRecordingsDir,
  normalizedCandidatePath,
  candidateReal,
  fastPathResult: isPathInsideDirectory(normalizedCandidatePath, canonicalRecordingsDir),
  realPathInsideRecordings: isPathInsideDirectory(candidateReal, canonicalRecordingsDir),
}, null, 2));
JS

Repository: webadderallorg/Recordly

Length of output: 8878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the existing tests around media authorization and symlinks.
sed -n '1,220p' electron/ipc/project/manager.test.ts

Repository: webadderallorg/Recordly

Length of output: 8265


Canonicalize candidatePath before the recordings-dir fast path. A symlink inside the configured recordings directory can satisfy the lexical prefix check here while resolving outside the allowlist, so this bypasses isAllowedLocalReadPath’s canonical-path guard. Add a regression test for a symlink under the recordings directory that points to a file outside it.

🤖 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/ipc/project/manager.ts` around lines 103 - 112, Update the
recordings-directory check in the path-allowance flow to canonicalize
candidatePath before evaluating isPathInsideDirectory, ensuring symlinks resolve
within the configured recordings root rather than bypassing
isAllowedLocalReadPath’s canonical guard. Preserve the existing fallback
behavior for unavailable real paths, and add a regression test covering a
symlink under the recordings directory targeting a file outside it.

}

return isAllowedLocalReadPath(normalizedCandidatePath);
}

Expand Down