Skip to content
Draft
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
26 changes: 26 additions & 0 deletions PR_BODY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
### WHY are these changes introduced?

The `readStdinString` function was reading all data from standard input without any size limitations, which could lead to memory exhaustion and Denial of Service (DoS) if processing untrusted or excessively large input.

### WHAT is this pull request doing?

Introduces a 10MB limit to `readStdinString`. It now tracks total bytes read and throws an `AbortError` if the limit is exceeded.

### How to test your changes?

Run the unit tests for `cli-kit`:
`pnpm --filter @shopify/cli-kit run vitest src/public/node/system.test.ts`

### Duplicate check

Query: `gh pr list --repo Shopify/cli --state all --limit 200 --search 'label:"Jules" "Security" in:title' --json number,title,state,url,body,files` (Note: `gh` was unavailable, so `git log` was used as fallback).

Closest prior PRs:
1. `f4de6ef1a`: [Security] Fix command injection in treeKill utility - Different subsystem (process management) and vulnerability class (command injection).
2. `e00baf901`: [Security] Fix command injection risk in cloudflared installer - Different subsystem and vulnerability class.

### Checklist

- [x] I've considered possible cross-platform impacts (Mac, Linux, Windows)
- [x] I've considered possible [documentation](https://shopify.dev) changes
- [x] I've considered analytics changes to measure impact
15 changes: 15 additions & 0 deletions packages/cli-kit/src/public/node/system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,19 @@ describe('readStdinString', () => {
// Then
expect(got).toBe('hello world')
})

test('throws AbortError when stdin content exceeds the limit', async () => {
// Given
vi.mocked(fs.fstatSync).mockReturnValue({isFIFO: () => true, isFile: () => false} as fs.Stats)
// Create a chunk that is larger than 10MB
const largeChunk = 'a'.repeat(10 * 1024 * 1024 + 1)
const mockStdin = Readable.from([largeChunk])
vi.spyOn(process, 'stdin', 'get').mockReturnValue(mockStdin as unknown as typeof process.stdin)

// When
const got = system.readStdinString()

// Then
await expect(got).rejects.toThrow('Stdin input exceeded the maximum allowed size.')
})
})
15 changes: 14 additions & 1 deletion packages/cli-kit/src/public/node/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import {delimiter} from 'pathe'
import {fstatSync} from 'fs'
import type {Writable, Readable} from 'stream'

/**
* The maximum size of data that can be read from stdin in bytes.
* This is to prevent memory exhaustion when reading from stdin.
* 10MB.
*/
const MAX_STDIN_SIZE = 10 * 1024 * 1024

export interface ExecOptions {
cwd?: string
env?: Record<string, string | undefined>
Expand Down Expand Up @@ -425,9 +432,15 @@ export async function readStdinString(): Promise<string | undefined> {
}

let data = ''
let totalSize = 0
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin) {
data += String(chunk)
const chunkString = String(chunk)
totalSize += Buffer.byteLength(chunkString, 'utf8')
if (totalSize > MAX_STDIN_SIZE) {
throw new AbortError('Stdin input exceeded the maximum allowed size.')
}
data += chunkString
}
return data.trim()
}
Loading