Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ package dev.nucleusframework.updater.internal

import java.io.File

/**
* Escapes a value for safe interpolation inside a **single-quoted** PowerShell string.
*
* The artifact path is derived from the `url` field of the remote update manifest, so a
* hostile or compromised manifest could otherwise embed a `'` to break out of the quoting
* and inject PowerShell that runs at install time. In a single-quoted PowerShell string a
* literal quote is written as two quotes, so doubling every `'` closes the injection while
* leaving ordinary Windows paths unchanged.
*/
internal fun psSingleQuote(value: String): String = value.replace("'", "''")

/**
* PowerShell that waits for the current process, runs the downloaded installer,
* optionally relaunches, then deletes the artifact and itself.
Expand All @@ -25,25 +36,27 @@ internal fun buildWindowsUpdateScript(
|$installerCommand
|$relaunchCommand
|# Clean up
|Remove-Item '$artifactPath' -Force -ErrorAction SilentlyContinue
|Remove-Item '$scriptPath' -Force -ErrorAction SilentlyContinue
|Remove-Item '${psSingleQuote(artifactPath)}' -Force -ErrorAction SilentlyContinue
|Remove-Item '${psSingleQuote(scriptPath)}' -Force -ErrorAction SilentlyContinue
""".trimMargin()

internal fun windowsInstallerCommand(
file: File,
extension: String,
): String =
when (extension) {
"msi" -> "Start-Process msiexec -ArgumentList '/i', '\"${file.absolutePath}\"', '/passive' -Wait"
else -> "Start-Process '${file.absolutePath}' -ArgumentList '/S', '--updated' -Wait"
): String {
val path = psSingleQuote(file.absolutePath)
return when (extension) {
"msi" -> "Start-Process msiexec -ArgumentList '/i', '\"$path\"', '/passive' -Wait"
else -> "Start-Process '$path' -ArgumentList '/S', '--updated' -Wait"
}
}

internal fun windowsRelaunchCommand(
restart: Boolean,
launcher: String?,
): String =
if (restart && launcher != null) {
"\n# Relaunch the application\nStart-Process '$launcher'"
"\n# Relaunch the application\nStart-Process '${psSingleQuote(launcher)}'"
} else {
""
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.nucleusframework.updater.internal

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
Expand Down Expand Up @@ -54,4 +55,22 @@ class WindowsUpdateScriptTest {
assertTrue("script cleanup: $script", script.contains("Remove-Item '${scriptFile.absolutePath}'"))
assertFalse("unexpected msiexec: $script", script.contains("msiexec"))
}

@Test
fun `a single quote in the manifest-derived name cannot break out of the powershell string`() {
// The artifact name comes from the remote manifest's `url`; a hostile `'` must be neutralised.
val artifact = File("C:\\\\Temp\\\\ev'il; Start-Process calc.exe #.exe")
val script =
buildWindowsUpdateScript(
pid = 1L,
installerCommand = windowsInstallerCommand(artifact, "exe"),
relaunchCommand = "",
artifactPath = artifact.absolutePath,
scriptPath = "C:\\\\Temp\\\\nucleus-update.ps1",
)
// The lone quote is doubled (escaped); the raw break-out sequence never appears verbatim.
assertTrue("escaped: $script", script.contains("ev''il; Start-Process calc.exe #.exe"))
assertFalse("raw quote break-out: $script", script.contains("ev'il;"))
assertEquals("ev''il", psSingleQuote("ev'il"))
}
}
Loading