Harden remote API security boundaries

This commit is contained in:
Bohdan Triapitsyn
2026-06-12 18:24:07 +03:00
parent c281937406
commit 106b31a407
52 changed files with 1582 additions and 579 deletions
+41 -27
View File
@@ -43,9 +43,10 @@ import { useFileSearchStore } from '@/stores/useFileSearchStore';
import { useDeviceInfo } from '@/lib/device';
import { cn, getModifierLabel, getRevealLabelKey, hasModifier } from '@/lib/utils';
import { getLanguageFromExtension, getImageMimeType, isDrawioFile, isImageFile } from '@/lib/toolHelpers';
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
import { refreshRuntimeUrlAuthToken } from '@/lib/runtime-auth';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import { getRuntimeUrlResolver } from '@/lib/runtime-url';
import { refreshRuntimeUrlAuthToken } from '@/lib/runtime-auth';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import { getOutsideFileGrant } from '@/lib/outsideFileGrants';
import { DiagramEditor } from '@/components/diagram';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { EditorView } from '@codemirror/view';
@@ -770,13 +771,17 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const openFiles = React.useMemo(() => openPaths.map(toFileNode), [openPaths, toFileNode]);
const effectiveSelectedPath = React.useMemo(() => selectedPath ?? openPaths[0] ?? null, [openPaths, selectedPath]);
const selectedFile = React.useMemo(() => (effectiveSelectedPath ? toFileNode(effectiveSelectedPath) : null), [effectiveSelectedPath, toFileNode]);
const selectedFilePath = selectedFile?.path ?? '';
const selectedFileIsOutsideWorkspace = Boolean(root && selectedFilePath && !isPathWithinRoot(selectedFilePath, root));
const selectedFileReadOptions = React.useMemo(
() => ({ allowOutsideWorkspace: mode === 'editor-only' && selectedFileIsOutsideWorkspace }),
[mode, selectedFileIsOutsideWorkspace],
);
const selectedFile = React.useMemo(() => (effectiveSelectedPath ? toFileNode(effectiveSelectedPath) : null), [effectiveSelectedPath, toFileNode]);
const selectedFilePath = selectedFile?.path ?? '';
const selectedFileIsOutsideWorkspace = Boolean(root && selectedFilePath && !isPathWithinRoot(selectedFilePath, root));
const selectedOutsideFileGrant = selectedFileIsOutsideWorkspace ? getOutsideFileGrant(selectedFilePath) : undefined;
const selectedFileReadOptions = React.useMemo(
() => ({
allowOutsideWorkspace: mode === 'editor-only' && selectedFileIsOutsideWorkspace,
outsideFileGrant: selectedOutsideFileGrant,
}),
[mode, selectedFileIsOutsideWorkspace, selectedOutsideFileGrant],
);
// Editor tabs horizontal scroll fades
const editorTabsScrollRef = React.useRef<HTMLDivElement>(null);
@@ -1444,16 +1449,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
};
}, [currentDirectory, debouncedSearchQuery, searchFiles, showHidden, showGitignored]);
const readFile = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean; optional?: boolean }): Promise<string> => {
const readFile = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean; outsideFileGrant?: string; optional?: boolean }): Promise<string> => {
if (files.readFile) {
const result = await files.readFile(path, options);
return result.content ?? '';
}
const params = new URLSearchParams({ path });
if (options?.allowOutsideWorkspace) {
params.set('allowOutsideWorkspace', 'true');
}
if (options?.allowOutsideWorkspace) {
params.set('allowOutsideWorkspace', 'true');
}
if (options?.outsideFileGrant) {
params.set('outsideFileGrant', options.outsideFileGrant);
}
if (options?.optional) {
params.set('optional', 'true');
}
@@ -1468,7 +1476,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return response.text();
}, [files, t]);
const readFileStat = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean }): Promise<FileStatSnapshot | null> => {
const readFileStat = React.useCallback(async (path: string, options?: { allowOutsideWorkspace?: boolean; outsideFileGrant?: string }): Promise<FileStatSnapshot | null> => {
if (files.statFile) {
const result = await files.statFile(path, options);
return {
@@ -1711,7 +1719,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
setFileLoading(true);
const readOptions = { allowOutsideWorkspace: mode === 'editor-only' && Boolean(root) && !isPathWithinRoot(node.path, root) };
const outsideFileGrant = getOutsideFileGrant(node.path);
const readOptions = {
allowOutsideWorkspace: mode === 'editor-only' && Boolean(root) && !isPathWithinRoot(node.path, root),
outsideFileGrant,
};
await readFile(node.path, readOptions)
.then((content) => {
@@ -2798,9 +2810,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
[lightTheme.metadata.id, darkTheme.metadata.id],
);
const imageAssetAuthKey = selectedFile?.path && isSelectedImage && !runtime.isDesktop && !isSelectedSvg
? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}`
: '';
const imageAssetAuthKey = selectedFile?.path && isSelectedImage && !runtime.isDesktop && !isSelectedSvg
? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}|${selectedFileReadOptions.outsideFileGrant ?? ''}`
: '';
React.useEffect(() => {
if (!imageAssetAuthKey) {
@@ -2830,10 +2842,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
: desktopImageSrc)
: (isSelectedSvg
? `data:${getImageMimeType(selectedFile.path)};utf8,${encodeURIComponent(fileContent)}`
: imageAssetAuthReadyKey === imageAssetAuthKey ? getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
}) : ''))
: imageAssetAuthReadyKey === imageAssetAuthKey ? getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
outsideFileGrant: selectedFileReadOptions.outsideFileGrant,
}) : ''))
: '';
React.useEffect(() => {
@@ -2849,10 +2862,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const srcPromise = files.readFileBinary
? files.readFileBinary(selectedFile.path, selectedFileReadOptions).then((result) => result.dataUrl)
: Promise.resolve(getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
}));
: Promise.resolve(getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
outsideFileGrant: selectedFileReadOptions.outsideFileGrant,
}));
await srcPromise
.then((src) => {
@@ -18,7 +18,7 @@ import { Icon } from "@/components/icon/Icon";
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
import { execCommand } from '@/lib/execCommands';
import { getGitCommitSummaries } from '@/lib/gitApi';
import { renderMagicPrompt } from '@/lib/magicPrompts';
import {
abortIntegrate,
@@ -143,18 +143,7 @@ export const IntegrateCommitsSection: React.FC<{
const max = 50;
// Show newest -> oldest.
const subset = plan.commits.slice(-max).reverse();
const quoted = subset.map((s) => JSON.stringify(s)).join(' ');
const result = await execCommand(
`git show -s --format=%H%x09%h%x09%s ${quoted}`,
repoRoot
);
const lines = (result.stdout || '').split(/\r?\n/).filter(Boolean);
const parsed: Array<{ sha: string; short: string; subject: string }> = [];
for (const line of lines) {
const [sha, short, subject] = line.split('\t');
if (!sha || !short) continue;
parsed.push({ sha, short, subject: subject || '' });
}
const parsed = await getGitCommitSummaries(repoRoot, subset);
if (!cancelled) {
setCommitSummaries(parsed);
setShowAllCommits(false);