mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-09-06 16:02:51 +08:00
120 lines
4.0 KiB
JavaScript
120 lines
4.0 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawnSync } from 'node:child_process'
|
|
import {
|
|
buildPosixShellCommand,
|
|
buildRsyncCommand,
|
|
quotePosixShellArg,
|
|
validateRsyncCommandInput
|
|
} from '../app/utils/rsync-command.js'
|
|
|
|
function testShellQuotingPreservesArguments() {
|
|
const expectedArgs = [
|
|
'',
|
|
'path with spaces',
|
|
'single\'quote',
|
|
'; printf INJECTED',
|
|
'$(printf INJECTED)',
|
|
'`printf INJECTED`',
|
|
'--leading-option',
|
|
'line1\nline2'
|
|
]
|
|
const environmentValue = 'value\'; printf ENV_INJECTED; #\''
|
|
const command = buildPosixShellCommand([
|
|
process.execPath,
|
|
'-e',
|
|
'process.stdout.write(JSON.stringify({ args: process.argv.slice(1), environment: process.env.SHELL_QUOTE_TEST }))',
|
|
'--',
|
|
...expectedArgs
|
|
], { SHELL_QUOTE_TEST: environmentValue })
|
|
const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8' })
|
|
|
|
assert.equal(result.status, 0, result.stderr)
|
|
assert.deepEqual(JSON.parse(result.stdout), { args: expectedArgs, environment: environmentValue })
|
|
assert.equal(quotePosixShellArg('a\'b'), '\'a\'"\'"\'b\'')
|
|
}
|
|
|
|
function testPasswordCommandEscapesAllUserControlledValues() {
|
|
const result = buildRsyncCommand({
|
|
sourcePaths: [
|
|
{ path: '/data/normal file.txt' },
|
|
{ path: '/data/\'; printf SOURCE_INJECTED; #\'' }
|
|
],
|
|
targetPath: '/backup/$(printf TARGET_INJECTED)/it\'s here',
|
|
targetOptions: {
|
|
host: 'backup.example.com',
|
|
port: '2222',
|
|
username: 'deploy-user',
|
|
password: 'p@ss\'; printf PASSWORD_INJECTED; #\''
|
|
},
|
|
transferOptions: {
|
|
delete: true,
|
|
excludePatterns: ['*.tmp', '\'; printf EXCLUDE_INJECTED; #\'']
|
|
}
|
|
})
|
|
|
|
assert.equal(result.environmentKeys.length, 1)
|
|
assert.equal(result.environmentKeys[0], 'SSHPASS')
|
|
assert.ok(result.commandArgs.includes('-s'))
|
|
assert.ok(result.commandArgs.includes('--delete'))
|
|
assert.ok(result.commandArgs.includes('--'))
|
|
assert.equal(result.commandArgs.at(-1), '[email protected]:/backup/$(printf TARGET_INJECTED)/it\'s here')
|
|
assert.ok(!result.command.includes('PASSWORD_INJECTED; #\' rsync'))
|
|
}
|
|
|
|
function testKeyCommandAndIpv6Destination() {
|
|
const result = buildRsyncCommand({
|
|
sourcePaths: [{ path: '/data/source' }],
|
|
targetPath: '/backup',
|
|
targetOptions: {
|
|
host: '2001:db8::1',
|
|
port: 22,
|
|
username: 'root',
|
|
privateKey: 'unused-by-builder'
|
|
},
|
|
keyFile: '/tmp/easynode_key_123_abc'
|
|
})
|
|
|
|
assert.deepEqual(result.environmentKeys, [])
|
|
assert.equal(result.commandArgs.at(-1), 'root@[2001:db8::1]:/backup')
|
|
const remoteShellIndex = result.commandArgs.indexOf('-e')
|
|
assert.match(result.commandArgs[remoteShellIndex + 1], /-i \/tmp\/easynode_key_123_abc/)
|
|
}
|
|
|
|
function testInvalidConnectionValuesAreRejected() {
|
|
const base = {
|
|
sourcePaths: [{ path: '/data/source' }],
|
|
targetPath: '/backup',
|
|
targetOptions: { host: 'example.com', port: 22, username: 'root', password: 'secret' }
|
|
}
|
|
|
|
assert.throws(() => buildRsyncCommand({
|
|
...base,
|
|
targetOptions: { ...base.targetOptions, host: 'host; printf INJECTED' }
|
|
}), /目标主机地址格式无效/)
|
|
assert.throws(() => buildRsyncCommand({
|
|
...base,
|
|
targetOptions: { ...base.targetOptions, username: '-oProxyCommand=bad' }
|
|
}), /目标主机用户名格式无效/)
|
|
assert.throws(() => buildRsyncCommand({
|
|
...base,
|
|
targetOptions: { ...base.targetOptions, port: '22; printf INJECTED' }
|
|
}), /目标主机端口/)
|
|
assert.throws(() => buildRsyncCommand({
|
|
...base,
|
|
sourcePaths: [{ path: '/data/source\0bad' }]
|
|
}), /空字节/)
|
|
assert.throws(() => buildRsyncCommand({
|
|
...base,
|
|
targetOptions: { ...base.targetOptions, password: '', privateKey: 'key' },
|
|
keyFile: '/tmp/key; printf INJECTED'
|
|
}), /临时密钥路径格式无效/)
|
|
assert.doesNotThrow(() => validateRsyncCommandInput(base))
|
|
}
|
|
|
|
testShellQuotingPreservesArguments()
|
|
testPasswordCommandEscapesAllUserControlledValues()
|
|
testKeyCommandAndIpv6Destination()
|
|
testInvalidConnectionValuesAreRejected()
|
|
|
|
console.log('rsync command security tests passed')
|