Skip to content

Commit 8bff732

Browse files
lacia-hIEluxuncangghostwriternr
authored
fix: Handle encoding parameter correctly in writeFile and readFile me… (#167)
* fix: Handle encoding parameter correctly in writeFile and readFile methods * fix: validate base64 content and respect encoding parameter in file operations - Add base64 content validation in writeFile to prevent command injection - Validate content contains only valid base64 characters (A-Z, a-z, 0-9, +, /, =) - Return VALIDATION_FAILED error for invalid content - Change from 'echo' to 'printf' for safer shell handling - Support user-specified encoding in readFile - Respect encoding parameter (base64/utf-8/utf8) over MIME detection - Allow forcing base64 for text files or utf-8 for binary files - Maintain backward compatibility with auto-detection when no encoding specified - Add comprehensive test coverage - 4 tests for encoding parameter support - 5 tests for base64 validation and security - All tests passing (35/35 in file-service.test.ts) Addresses reviewer feedback from PR #167: - Sanitize base64 content to prevent command injection attacks - Remove unused variable declarations * Update lockfile * Clean up encoding logic and improve comments Remove redundant intermediate variables and assignments in read/write. Clarify shell command comments to accurately explain printf usage and base64 encoding rationale. Use printf consistently for both paths. * Document build system reliability in CLAUDE.md * Remove encoding defaults to enable MIME auto-detection Remove 'utf8'/'utf-8' defaults from SDK client and HTTP handler, allowing file service to perform MIME-based detection when encoding is not explicitly specified. This fixes binary file detection. --------- Co-authored-by: luxuncang <[email protected]> Co-authored-by: Naresh <[email protected]>
1 parent 89632aa commit 8bff732

File tree

6 files changed

+286
-36
lines changed

6 files changed

+286
-36
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'should handle cl
217217
- Sequential execution (`singleFork: true`) to prevent container resource contention
218218
- Longer timeouts (2min per test) for container operations
219219

220+
**Build system trust:** The monorepo build system (turbo + npm workspaces) is robust and handles all package dependencies automatically. E2E tests always run against the latest built code - there's no need to manually rebuild or worry about stale builds unless explicitly working on the build setup itself.
221+
220222
**CI behavior:** E2E tests in CI (`pullrequest.yml`):
221223

222224
1. Build Docker image locally (`npm run docker:local`)

package-lock.json

Lines changed: 5 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sandbox-container/src/handlers/file-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class FileHandler extends BaseHandler<Request, Response> {
7575
const body = await this.parseRequestBody<ReadFileRequest>(request);
7676

7777
const result = await this.fileService.readFile(body.path, {
78-
encoding: body.encoding || 'utf-8'
78+
encoding: body.encoding
7979
});
8080

8181
if (result.success) {
@@ -191,7 +191,7 @@ export class FileHandler extends BaseHandler<Request, Response> {
191191
const body = await this.parseRequestBody<WriteFileRequest>(request);
192192

193193
const result = await this.fileService.writeFile(body.path, body.content, {
194-
encoding: body.encoding || 'utf-8'
194+
encoding: body.encoding
195195
});
196196

197197
if (result.success) {

packages/sandbox-container/src/services/file-service.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,19 @@ export class FileService implements FileSystemOperations {
219219
!mimeType.includes('x-empty');
220220

221221
// 6. Read file with appropriate encoding
222-
let content: string;
222+
// Respect user's encoding preference if provided, otherwise use MIME-based detection
223223
let actualEncoding: 'utf-8' | 'base64';
224+
if (options.encoding === 'base64') {
225+
actualEncoding = 'base64';
226+
} else if (options.encoding === 'utf-8' || options.encoding === 'utf8') {
227+
actualEncoding = 'utf-8';
228+
} else {
229+
// No explicit encoding requested - use MIME-based detection (original behavior)
230+
actualEncoding = isBinary ? 'base64' : 'utf-8';
231+
}
224232

225-
if (isBinary) {
233+
let content: string;
234+
if (actualEncoding === 'base64') {
226235
// Binary files: read as base64, return as-is (DO NOT decode)
227236
const base64Command = `base64 -w 0 < ${escapedPath}`;
228237
const base64Result = await this.sessionManager.executeInSession(
@@ -261,7 +270,6 @@ export class FileService implements FileSystemOperations {
261270
}
262271

263272
content = base64Result.data.stdout.trim();
264-
actualEncoding = 'base64';
265273
} else {
266274
// Text files: read normally
267275
const catCommand = `cat ${escapedPath}`;
@@ -301,15 +309,14 @@ export class FileService implements FileSystemOperations {
301309
}
302310

303311
content = catResult.data.stdout;
304-
actualEncoding = 'utf-8';
305312
}
306313

307314
return {
308315
success: true,
309316
data: content,
310317
metadata: {
311318
encoding: actualEncoding,
312-
isBinary,
319+
isBinary: actualEncoding === 'base64',
313320
mimeType,
314321
size: fileSize
315322
}
@@ -366,12 +373,41 @@ export class FileService implements FileSystemOperations {
366373
};
367374
}
368375

369-
// 2. Write file using SessionManager with base64 encoding
370-
// Base64 ensures binary files (images, PDFs, etc.) are written correctly
371-
// and avoids heredoc EOF collision issues
376+
// 2. Write file using SessionManager with proper encoding handling
372377
const escapedPath = this.escapePath(path);
373-
const base64Content = Buffer.from(content, 'utf-8').toString('base64');
374-
const command = `echo '${base64Content}' | base64 -d > ${escapedPath}`;
378+
const encoding = options.encoding || 'utf-8';
379+
380+
let command: string;
381+
382+
if (encoding === 'base64') {
383+
// Content is already base64 encoded, validate and decode it directly to file
384+
// Validate that content only contains valid base64 characters to prevent command injection
385+
if (!/^[A-Za-z0-9+/=]*$/.test(content)) {
386+
return {
387+
success: false,
388+
error: {
389+
message: `Invalid base64 content for '${path}': must contain only A-Z, a-z, 0-9, +, /, =`,
390+
code: ErrorCode.VALIDATION_FAILED,
391+
details: {
392+
validationErrors: [
393+
{
394+
field: 'content',
395+
message: 'Invalid base64 characters',
396+
code: 'INVALID_BASE64'
397+
}
398+
]
399+
} satisfies ValidationFailedContext
400+
}
401+
};
402+
}
403+
// Use printf to output base64 literally without trailing newline
404+
command = `printf '%s' '${content}' | base64 -d > ${escapedPath}`;
405+
} else {
406+
// Encode text to base64 to safely handle shell metacharacters (quotes, backticks, $, etc.)
407+
// and special characters (newlines, control chars, null bytes) in user content
408+
const base64Content = Buffer.from(content, 'utf-8').toString('base64');
409+
command = `printf '%s' '${base64Content}' | base64 -d > ${escapedPath}`;
410+
}
375411

376412
const execResult = await this.sessionManager.executeInSession(
377413
sessionId,

0 commit comments

Comments
 (0)