Harden remote API security boundaries
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user