Skip to content

Commit 5c211ea

Browse files
authored
Merge commit from fork
* fix: command injection in runNpm Fix security vulnerability where including an escape sequence in app store install command would allow executing arbitrary code on the server by including it in the url. * fix: add allowConfigure to restore Protect validateBackup and restore with securityStrategy.allowConfigure. * fix: remove restore global state Remove shared, global state from validate & restore sequence by using a restoresession stored in a cookie.
1 parent f06140b commit 5c211ea

2 files changed

Lines changed: 59 additions & 16 deletions

File tree

src/modules.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,19 +193,27 @@ export function runNpm(
193193

194194
debug(`${command}: ${packageString}`)
195195

196+
const npmArgs = isTheServerModule(name, config)
197+
? [command, '-g']
198+
: ['--save', command]
199+
200+
if (packageString) {
201+
npmArgs.push(packageString)
202+
}
203+
196204
if (isTheServerModule(name, config)) {
197205
if (process.platform === 'win32') {
198-
npm = spawn('cmd', ['/c', `npm ${command} -g ${packageString} `], opts)
206+
npm = spawn('npm.cmd', npmArgs, opts)
199207
} else {
200-
npm = spawn('sudo', ['npm', command, '-g', packageString], opts)
208+
npm = spawn('sudo', ['npm', ...npmArgs], opts)
201209
}
202210
} else {
203211
opts.cwd = config.configPath
204212

205213
if (process.platform === 'win32') {
206-
npm = spawn('cmd', ['/c', `npm --save ${command} ${packageString}`], opts)
214+
npm = spawn('npm.cmd', npmArgs, opts)
207215
} else {
208-
npm = spawn('npm', ['--save', command, packageString], opts)
216+
npm = spawn('npm', npmArgs, opts)
209217
}
210218
}
211219

src/serverroutes.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ module.exports = function (
9494
getSecurityConfig: SecurityConfigGetter
9595
) {
9696
let securityWasEnabled = false
97-
let restoreFilePath: string
97+
const restoreSessions = new Map<string, string>()
9898

9999
const logopath = path.resolve(app.config.configPath, 'logo.svg')
100100
if (fs.existsSync(logopath)) {
@@ -1008,16 +1008,28 @@ module.exports = function (
10081008
})
10091009
}
10101010

1011-
app.post(`${SERVERROUTESPREFIX}/restore`, (req: Request, res: Response) => {
1012-
if (!restoreFilePath) {
1013-
res.status(400).send('not exting restore file')
1014-
} else if (!fs.existsSync(restoreFilePath)) {
1015-
res.status(400).send('restore file does not exist')
1016-
} else {
1017-
res.status(202).send()
1018-
}
1011+
app.post(
1012+
`${SERVERROUTESPREFIX}/restore`,
1013+
(req: Request, res: Response) => {
1014+
if (
1015+
!app.securityStrategy.isDummy() &&
1016+
!app.securityStrategy.allowConfigure(req)
1017+
) {
1018+
res.status(401).send('Restore not allowed')
1019+
return
1020+
}
1021+
const sessionId = getCookie(req, 'restoreSession')
1022+
const restoreFilePath = sessionId ? restoreSessions.get(sessionId) : undefined
1023+
1024+
if (!restoreFilePath) {
1025+
res.status(400).send('not exting restore file')
1026+
} else if (!fs.existsSync(restoreFilePath)) {
1027+
res.status(400).send('restore file does not exist')
1028+
} else {
1029+
res.status(202).send()
1030+
}
10191031

1020-
listSafeRestoreFiles(restoreFilePath)
1032+
listSafeRestoreFiles(restoreFilePath!)
10211033
.then((files) => {
10221034
const wanted = files.filter((name) => {
10231035
return req.body[name]
@@ -1032,7 +1044,7 @@ module.exports = function (
10321044
i / wanted.length
10331045
)
10341046
ncp(
1035-
path.join(restoreFilePath, name),
1047+
path.join(restoreFilePath!, name),
10361048
path.join(app.config.configPath, name),
10371049
{ stopOnErr: true },
10381050
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1072,6 +1084,13 @@ module.exports = function (
10721084
app.post(
10731085
`${SERVERROUTESPREFIX}/validateBackup`,
10741086
(req: Request, res: Response) => {
1087+
if (
1088+
!app.securityStrategy.isDummy() &&
1089+
!app.securityStrategy.allowConfigure(req)
1090+
) {
1091+
res.status(401).send('Validate backup not allowed')
1092+
return
1093+
}
10751094
const bb = busboy({ headers: req.headers })
10761095
bb.on(
10771096
'file',
@@ -1095,7 +1114,12 @@ module.exports = function (
10951114
return
10961115
}
10971116
const tmpDir = os.tmpdir()
1098-
restoreFilePath = fs.mkdtempSync(`${tmpDir}${path.sep}`)
1117+
const restoreFilePath = fs.mkdtempSync(`${tmpDir}${path.sep}`)
1118+
const sessionId = Date.now() + '_' + Math.random().toString(36).substr(2, 9)
1119+
restoreSessions.set(sessionId, restoreFilePath)
1120+
setTimeout(() => restoreSessions.delete(sessionId), 15 * 60 * 1000)
1121+
res.cookie('restoreSession', sessionId, { httpOnly: true, sameSite: 'strict' })
1122+
10991123
const zipFileDir = fs.mkdtempSync(`${tmpDir}${path.sep}`)
11001124
const zipFile = path.join(zipFileDir, 'backup.zip')
11011125
const unzipStream = unzipper.Extract({ path: restoreFilePath })
@@ -1185,3 +1209,14 @@ const setNoCache = (res: Response) => {
11851209
res.header('Pragma', 'no-cache')
11861210
res.header('Expires', '0')
11871211
}
1212+
1213+
function getCookie(req: Request, name: string): string | undefined {
1214+
if (req.headers.cookie) {
1215+
const value = '; ' + req.headers.cookie
1216+
const parts = value.split('; ' + name + '=')
1217+
if (parts.length === 2) {
1218+
return parts.pop()?.split(';').shift()
1219+
}
1220+
}
1221+
return undefined
1222+
}

0 commit comments

Comments
 (0)