From e7386696b6b6c5ae442ab0a67ea8bdbfc7a4cf1e Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 28 May 2026 16:28:10 +0000 Subject: [PATCH 1/9] fix(java_opts): preserve quoting and prevent JAVA_OPTS glob expansion (#1301) - Replace xargs-based JAVA_OPTS normalization with newline-only normalization to preserve quotes and backslashes. - Protect user-provided JAVA_OPTS during opts-file eval expansion by using a placeholder and restoring it after eval. - Introduce JavaExecCommand() and switch Java Main/Spring Boot start commands to eval "exec ... $JAVA_OPTS ..." so shell globbing/word-splitting does not corrupt args. - Add regression tests for quoted values, cron/glob expressions, multiple quoted args, backslashes, and start-command invocation behavior. - Clean up overlapping/no-op test code and improve test readability. --- src/java/containers/container.go | 12 +++ src/java/containers/java_main.go | 9 +-- src/java/containers/java_main_test.go | 61 +++++++++++++++ src/java/containers/spring_boot.go | 13 +--- src/java/containers/spring_boot_test.go | 16 ++++ src/java/frameworks/java_opts_writer.go | 18 +++-- src/java/frameworks/java_opts_writer_test.go | 79 +++++++++++++++++--- 7 files changed, 176 insertions(+), 32 deletions(-) diff --git a/src/java/containers/container.go b/src/java/containers/container.go index e16fefe05..84cadd8de 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -127,6 +127,18 @@ func (r *Registry) RegisterStandardContainers() { r.Register(NewJavaMainContainer(r.context)) } +// JavaExecCommand builds a start command of the form: +// +// eval "exec $JAVA_HOME/bin/java $JAVA_OPTS " +// +// Wrapping the argument to eval in double quotes prevents bash from +// glob-expanding or word-splitting $JAVA_OPTS before eval sees it. +// eval then re-parses the string, honouring any embedded quotes in $JAVA_OPTS. +// javaArgs must be buildpack-generated command fragments (not untrusted input). +func JavaExecCommand(javaArgs string) string { + return `eval "exec $JAVA_HOME/bin/java $JAVA_OPTS ` + javaArgs + `"` +} + // This script is used to process the CLASSPATH assembled from various framework scripts sourced from profile.d // to further create symlinks to the corresponding framework dependencies in WEB-INF/lib, BOOT-INF/lib and where ever // needed thus they are available for application classloading diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index 99570f339..374c8250a 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -292,13 +292,11 @@ func (j *JavaMainContainer) Release() (string, error) { // JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class. // Use classpath mode so the configured class is actually invoked (not the manifest's). if cfg.JavaMainClass != "" { - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", cfg.JavaMainClass, args), nil + return JavaExecCommand(fmt.Sprintf("-cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", cfg.JavaMainClass, args)), nil } if j.jarFile != "" { - // JAR has its own Main-Class in the manifest — java -jar handles it - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s%s", j.jarFile, args), nil + return JavaExecCommand(fmt.Sprintf("-jar %s%s", j.jarFile, args)), nil } // Classpath mode: need an explicit main class @@ -311,6 +309,5 @@ func (j *JavaMainContainer) Release() (string, error) { j.context.Log.Debug("Main Class %s found in JAVA_MAIN_CLASS", mainClass) } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args), nil + return JavaExecCommand(fmt.Sprintf("-cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args)), nil } diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index 2a0d0f0cd..07e798a36 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -290,6 +290,10 @@ var _ = Describe("Java Main Container", func() { }) Describe("buildClasspath", func() { + expectQuotedEval := func(cmd string) { + Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) + } + Context("with JARs in root and lib/", func() { BeforeEach(func() { os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644) @@ -351,6 +355,63 @@ var _ = Describe("Java Main Container", func() { Expect(cmd).To(ContainSubstring(".")) }) }) + + // Regression tests for issue #1301: start command must use eval "exec ... $JAVA_OPTS" + // (quoted string) so that glob chars in JAVA_OPTS are not expanded by bash before eval. + Context("with JAR file (eval quoting)", func() { + BeforeEach(func() { + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JAVA_MAIN_CLASS env variable (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JAVA_MAIN_CLASS", "com.example.Main") + os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JAVA_MAIN_CLASS") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JBP_CONFIG_JAVA_MAIN java_main_class (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.Main}") + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) }) Describe("Finalize", func() { diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index ac6119c1c..e7a693c61 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -256,21 +256,16 @@ func (s *SpringBootContainer) Release() (string, error) { // Verify this is actually a Spring Boot application if s.isSpringBootExplodedJar(buildDir) { - // True Spring Boot exploded JAR - use main class from manifest or fallback to JarLauncher based on spring-boot version launcherClass := s.getLauncherClass(buildDir) - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", launcherClass), nil + return JavaExecCommand(fmt.Sprintf("-cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", launcherClass)), nil } - // Exploded JAR but NOT Spring Boot - use Main-Class from MANIFEST.MF mainClass, err := s.readMainClassFromManifest(buildDir) if err != nil { s.context.Log.Debug("Could not read MANIFEST.MF: %s", err.Error()) } if mainClass != "" { - // Use classpath from BOOT-INF/classes and BOOT-INF/lib - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil + return JavaExecCommand(fmt.Sprintf("-cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass)), nil } return "", fmt.Errorf("exploded JAR found but no Main-Class in MANIFEST.MF") @@ -292,8 +287,8 @@ func (s *SpringBootContainer) Release() (string, error) { jarFile = jar } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS ${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile) + // Use eval with quoted string to prevent glob-expansion of $JAVA_OPTS (#1301) + cmd := JavaExecCommand(fmt.Sprintf("${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile)) return cmd, nil } diff --git a/src/java/containers/spring_boot_test.go b/src/java/containers/spring_boot_test.go index 752dd1ec7..f3239a7ce 100644 --- a/src/java/containers/spring_boot_test.go +++ b/src/java/containers/spring_boot_test.go @@ -122,6 +122,10 @@ var _ = Describe("Spring Boot Container", func() { }) Describe("Release", func() { + expectQuotedEval := func(cmd string) { + Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) + } + Context("with exploded JAR (BOOT-INF)", func() { BeforeEach(func() { os.MkdirAll(filepath.Join(buildDir, "BOOT-INF"), 0755) @@ -136,6 +140,12 @@ var _ = Describe("Spring Boot Container", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmd).To(ContainSubstring("JarLauncher")) }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) }) Context("with Spring Boot JAR", func() { @@ -151,6 +161,12 @@ var _ = Describe("Spring Boot Container", func() { Expect(cmd).To(ContainSubstring("java")) Expect(cmd).To(ContainSubstring("app-boot.jar")) }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) }) Context("with no Spring Boot JAR found", func() { diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 4bfd0a6d1..2459b355e 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -74,8 +74,8 @@ func CreateJavaOptsAssemblyScript(ctx *common.Context) error { # Save original JAVA_OPTS from environment (user-provided) # Normalize to single line: YAML block scalars (>) may introduce newlines -# xargs trims leading/trailing whitespace and collapses internal spaces -USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ' | tr -s ' ' | xargs) +# Only convert newlines to spaces — do not use xargs which strips quotes and backslashes +USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ') # Start building new JAVA_OPTS JAVA_OPTS="" @@ -86,18 +86,26 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # Read content and expand runtime variables opts_content=$(cat "$opts_file") - # Expand $DEPS_DIR, $HOME, $JAVA_OPTS using bash parameter expansion. + # Expand $DEPS_DIR and $HOME using bash parameter expansion. # sed-based substitution breaks when these values contain the sed delimiter (|), # backslashes, ampersands, or newlines — all valid in JAVA_OPTS and paths. opts_content="${opts_content//\$DEPS_DIR/$DEPS_DIR}" opts_content="${opts_content//\$HOME/$HOME}" - opts_content="${opts_content//\$JAVA_OPTS/$USER_JAVA_OPTS}" - + + # Shield $JAVA_OPTS from eval: replace with a placeholder first, + # then substitute the actual value AFTER eval so that quotes and + # backslashes in the user-provided JAVA_OPTS are never exposed to eval. + _user_java_opts_placeholder='__JAVA_OPTS_BUILDPACK_PLACEHOLDER__' + opts_content="${opts_content//\$JAVA_OPTS/$_user_java_opts_placeholder}" + # Expand any remaining environment variables in opts content via eval. # Note: eval executes commands, but .opts files are written by the buildpack # at staging time and run within the container context. # This matches how the Ruby buildpack naturally expanded variables via shell. opts_content=$(eval "echo \"$opts_content\"") + + # Now safely substitute JAVA_OPTS after eval (preserves quotes and backslashes) + opts_content="${opts_content//$_user_java_opts_placeholder/$USER_JAVA_OPTS}" if [ -n "$opts_content" ]; then JAVA_OPTS="$JAVA_OPTS $opts_content" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index e092ec5df..59ed8e3ce 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -53,17 +54,8 @@ var _ = Describe("Java Opts Writer", func() { os.RemoveAll(depsDir) }) - Describe("Basic options", func() { - It("writes JAVA_OPTS correctly", func() { - javaOpts := "-Xmx512M -Xms256M" - os.Setenv("JAVA_OPTS", javaOpts) - - Expect(os.Getenv("JAVA_OPTS")).To(Equal(javaOpts)) - }) - }) - Describe("CreateJavaOptsAssemblyScript", func() { - runScript := func(javaOpts string, optsFileContent string) (string, error) { + setupScript := func(javaOpts string, optsFileContent string) string { err := frameworks.CreateJavaOptsAssemblyScript(ctx) Expect(err).NotTo(HaveOccurred()) @@ -71,8 +63,11 @@ var _ = Describe("Java Opts Writer", func() { Expect(os.MkdirAll(optsDir, 0755)).To(Succeed()) Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte(optsFileContent), 0644)).To(Succeed()) - scriptPath := filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") - cmd := exec.Command("bash", "-c", "source "+scriptPath+" && echo \"$JAVA_OPTS\"") + return filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") + } + + runWithEnv := func(scriptPath, javaOpts, bashExpr string) (string, error) { + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && "+bashExpr) cmd.Env = append(os.Environ(), "JAVA_OPTS="+javaOpts, "DEPS_DIR="+depsDir, @@ -82,6 +77,22 @@ var _ = Describe("Java Opts Writer", func() { return string(output), err } + runScript := func(javaOpts string, optsFileContent string) (string, error) { + scriptPath := setupScript(javaOpts, optsFileContent) + return runWithEnv(scriptPath, javaOpts, "echo \"$JAVA_OPTS\"") + } + + // runStartCommand simulates the actual JVM invocation: + // eval "exec $JAVA_HOME/bin/java $JAVA_OPTS -jar app.jar" + // Returns the argument list java would receive (one arg per line). + runStartCommand := func(javaOpts string, optsFileContent string) (string, error) { + scriptPath := setupScript(javaOpts, optsFileContent) + // Simulate: eval "exec java $JAVA_OPTS" — quoted string prevents bash glob-expansion. + // eval then re-parses the string, honouring embedded quotes in $JAVA_OPTS. + return runWithEnv(scriptPath, javaOpts, + `eval "set -- $JAVA_OPTS"; printf '%s\n' "$@"`) + } + It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { // Reproduce the manifest pattern: // JAVA_OPTS: > @@ -117,5 +128,49 @@ var _ = Describe("Java Opts Writer", func() { Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) Expect(output).To(ContainSubstring("-Djava.security.properties=" + depsDir + "/0/security.properties")) }) + + // Regression tests for issue #1301: xargs strips quotes, breaking quoted JVM args + It("preserves quoted value with spaces in JAVA_OPTS", func() { + // JAVA_OPTS='-Dfoo="bar baz"' — xargs removes the quotes from USER_JAVA_OPTS, + // so when eval exec java $JAVA_OPTS is called, -Dfoo=bar and baz become separate args + output, err := runScript(`-Dfoo="bar baz"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo="bar baz"`)) + }) + + It("preserves cron expression with glob characters in JAVA_OPTS", func() { + // JAVA_OPTS='-DcronSched="0 */7 * * * *"' — xargs strips quotes, then * expands via glob + // when eval exec java $JAVA_OPTS is invoked, corrupting the cron expression + output, err := runScript(`-DcronSched="0 */7 * * * *"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-DcronSched="0 */7 * * * *"`)) + }) + + It("preserves multiple quoted args in JAVA_OPTS", func() { + // Multiple quoted values — xargs strips all quotes, each space-containing value splits + output, err := runScript(`-Dfoo="bar baz" -Dother="qux quux"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo="bar baz"`)) + Expect(output).To(ContainSubstring(`-Dother="qux quux"`)) + }) + + It("preserves backslashes in JAVA_OPTS values", func() { + // xargs treats backslash as escape char: C:\path\to\app -> C:pathtoapp + // Affects regex patterns and any path using backslash notation + output, err := runScript(`-DregEx="[a-z]+(.*)" -Dpattern=foo\|bar`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-DregEx="[a-z]+(.*)"`)) + Expect(output).To(ContainSubstring(`foo\|bar`)) + }) + + // Full invocation cycle test for issue #1301: + // Verifies that the quoted eval "exec ... $JAVA_OPTS" form delivers the correct + // argument to java — glob chars in $JAVA_OPTS are not expanded. + It("does not glob-expand * in cron expression when invoking java", func() { + output, err := runStartCommand(`-DcronSched="0 */7 * * * *"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + // Java receives exactly one arg: -DcronSched=0 */7 * * * * + Expect(strings.TrimSpace(output)).To(Equal("-DcronSched=0 */7 * * * *")) + }) }) }) From 91fbd552e96a3cb131d07140653b9af02533cf9a Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 15:28:17 +0000 Subject: [PATCH 2/9] fix(java_opts): use printf for USER_JAVA_OPTS normalization --- src/java/frameworks/java_opts_writer.go | 2 +- src/java/frameworks/java_opts_writer_test.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 2459b355e..4e4822048 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -75,7 +75,7 @@ func CreateJavaOptsAssemblyScript(ctx *common.Context) error { # Save original JAVA_OPTS from environment (user-provided) # Normalize to single line: YAML block scalars (>) may introduce newlines # Only convert newlines to spaces — do not use xargs which strips quotes and backslashes -USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ') +USER_JAVA_OPTS=$(printf '%%s' "$JAVA_OPTS" | tr '\n' ' ') # Start building new JAVA_OPTS JAVA_OPTS="" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 59ed8e3ce..898144cf5 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -163,6 +163,12 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring(`foo\|bar`)) }) + It("preserves leading -n argument in JAVA_OPTS", func() { + output, err := runScript(`-n -Dfoo=bar`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-n -Dfoo=bar`)) + }) + // Full invocation cycle test for issue #1301: // Verifies that the quoted eval "exec ... $JAVA_OPTS" form delivers the correct // argument to java — glob chars in $JAVA_OPTS are not expanded. From 5503020b618d701896c9c553873b67e796e4ee85 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 15:32:30 +0000 Subject: [PATCH 3/9] fix(java_opts): use printf in eval expansion path --- src/java/frameworks/java_opts_writer.go | 2 +- src/java/frameworks/java_opts_writer_test.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 4e4822048..963730bd5 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -102,7 +102,7 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # Note: eval executes commands, but .opts files are written by the buildpack # at staging time and run within the container context. # This matches how the Ruby buildpack naturally expanded variables via shell. - opts_content=$(eval "echo \"$opts_content\"") + opts_content=$(eval "printf '%%s' \"$opts_content\"") # Now safely substitute JAVA_OPTS after eval (preserves quotes and backslashes) opts_content="${opts_content//$_user_java_opts_placeholder/$USER_JAVA_OPTS}" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 898144cf5..cc748d436 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -79,7 +79,7 @@ var _ = Describe("Java Opts Writer", func() { runScript := func(javaOpts string, optsFileContent string) (string, error) { scriptPath := setupScript(javaOpts, optsFileContent) - return runWithEnv(scriptPath, javaOpts, "echo \"$JAVA_OPTS\"") + return runWithEnv(scriptPath, javaOpts, `printf '%s\n' "$JAVA_OPTS"`) } // runStartCommand simulates the actual JVM invocation: @@ -129,6 +129,12 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring("-Djava.security.properties=" + depsDir + "/0/security.properties")) }) + It("preserves literal -n from opts file content", func() { + output, err := runScript("", "-n") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(strings.TrimSpace(output)).To(Equal("-n")) + }) + // Regression tests for issue #1301: xargs strips quotes, breaking quoted JVM args It("preserves quoted value with spaces in JAVA_OPTS", func() { // JAVA_OPTS='-Dfoo="bar baz"' — xargs removes the quotes from USER_JAVA_OPTS, From 083de4e554113a546bba35eb0260f71e6fb36f71 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 15:36:18 +0000 Subject: [PATCH 4/9] fix(java_opts): escape replacement chars in placeholder substitution --- src/java/frameworks/java_opts_writer.go | 6 ++++-- src/java/frameworks/java_opts_writer_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 963730bd5..d207a70ec 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -104,8 +104,10 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # This matches how the Ruby buildpack naturally expanded variables via shell. opts_content=$(eval "printf '%%s' \"$opts_content\"") - # Now safely substitute JAVA_OPTS after eval (preserves quotes and backslashes) - opts_content="${opts_content//$_user_java_opts_placeholder/$USER_JAVA_OPTS}" + # Now safely substitute JAVA_OPTS after eval (preserves quotes, backslashes, and ampersands) + _escaped_user_java_opts="${USER_JAVA_OPTS//\\/\\\\}" + _escaped_user_java_opts="${_escaped_user_java_opts//&/\\&}" + opts_content="${opts_content//$_user_java_opts_placeholder/$_escaped_user_java_opts}" if [ -n "$opts_content" ]; then JAVA_OPTS="$JAVA_OPTS $opts_content" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index cc748d436..86dcb3f5c 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -175,6 +175,18 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring(`-n -Dfoo=bar`)) }) + It("preserves ampersand in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dfoo=a&b`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo=a&b`)) + }) + + It("preserves backslashes in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dpath=C:\tmp\app`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpath=C:\tmp\app`)) + }) + // Full invocation cycle test for issue #1301: // Verifies that the quoted eval "exec ... $JAVA_OPTS" form delivers the correct // argument to java — glob chars in $JAVA_OPTS are not expanded. From 7183dc8ecb1874a253ca2eead327dc7b6505a04d Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 15:50:58 +0000 Subject: [PATCH 5/9] test/docs: clarify JavaExecCommand comment and align Release test placement (#1301) --- src/java/containers/container.go | 3 +- src/java/containers/java_main_test.go | 121 +++++++++++++------------- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/src/java/containers/container.go b/src/java/containers/container.go index 84cadd8de..06a7b84b7 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -134,7 +134,8 @@ func (r *Registry) RegisterStandardContainers() { // Wrapping the argument to eval in double quotes prevents bash from // glob-expanding or word-splitting $JAVA_OPTS before eval sees it. // eval then re-parses the string, honouring any embedded quotes in $JAVA_OPTS. -// javaArgs must be buildpack-generated command fragments (not untrusted input). +// javaArgs may include app/config-derived values (e.g. jar paths, Main-Class, +// configured arguments), so callers must ensure it is shell-safe. func JavaExecCommand(javaArgs string) string { return `eval "exec $JAVA_HOME/bin/java $JAVA_OPTS ` + javaArgs + `"` } diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index 07e798a36..ebd094507 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -154,6 +154,10 @@ var _ = Describe("Java Main Container", func() { }) Describe("Release", func() { + expectQuotedEval := func(cmd string) { + Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) + } + Context("with JAR file", func() { BeforeEach(func() { Expect(createJar( @@ -280,6 +284,63 @@ var _ = Describe("Java Main Container", func() { }) }) + // Regression tests for issue #1301: start command must use eval "exec ... $JAVA_OPTS" + // (quoted string) so that glob chars in JAVA_OPTS are not expanded by bash before eval. + Context("with JAR file (eval quoting)", func() { + BeforeEach(func() { + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JAVA_MAIN_CLASS env variable (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JAVA_MAIN_CLASS", "com.example.Main") + os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JAVA_MAIN_CLASS") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JBP_CONFIG_JAVA_MAIN java_main_class (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.Main}") + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + Context("without main class or JAR", func() { It("returns error", func() { _, err := container.Release() @@ -290,10 +351,6 @@ var _ = Describe("Java Main Container", func() { }) Describe("buildClasspath", func() { - expectQuotedEval := func(cmd string) { - Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) - } - Context("with JARs in root and lib/", func() { BeforeEach(func() { os.WriteFile(filepath.Join(buildDir, "app.jar"), []byte("fake"), 0644) @@ -356,62 +413,6 @@ var _ = Describe("Java Main Container", func() { }) }) - // Regression tests for issue #1301: start command must use eval "exec ... $JAVA_OPTS" - // (quoted string) so that glob chars in JAVA_OPTS are not expanded by bash before eval. - Context("with JAR file (eval quoting)", func() { - BeforeEach(func() { - Expect(createJar( - filepath.Join(buildDir, "app.jar"), - "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", - )).To(Succeed()) - container.Detect() - }) - - It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { - cmd, err := container.Release() - Expect(err).NotTo(HaveOccurred()) - expectQuotedEval(cmd) - }) - }) - - Context("with JAVA_MAIN_CLASS env variable (eval quoting)", func() { - BeforeEach(func() { - os.Setenv("JAVA_MAIN_CLASS", "com.example.Main") - os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644) - container.Detect() - }) - - AfterEach(func() { - os.Unsetenv("JAVA_MAIN_CLASS") - }) - - It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { - cmd, err := container.Release() - Expect(err).NotTo(HaveOccurred()) - expectQuotedEval(cmd) - }) - }) - - Context("with JBP_CONFIG_JAVA_MAIN java_main_class (eval quoting)", func() { - BeforeEach(func() { - os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.Main}") - Expect(createJar( - filepath.Join(buildDir, "app.jar"), - "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", - )).To(Succeed()) - container.Detect() - }) - - AfterEach(func() { - os.Unsetenv("JBP_CONFIG_JAVA_MAIN") - }) - - It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { - cmd, err := container.Release() - Expect(err).NotTo(HaveOccurred()) - expectQuotedEval(cmd) - }) - }) }) Describe("Finalize", func() { From 9741147d8edc90fdfed2297342a77ce29a1777d5 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 17:40:24 +0000 Subject: [PATCH 6/9] refactor(java_opts): hoist invariant escaping and add double-backslash regression test --- src/java/frameworks/java_opts_writer.go | 20 ++++++++++------ src/java/frameworks/java_opts_writer_test.go | 25 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index d207a70ec..f45a742a8 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -80,6 +80,15 @@ USER_JAVA_OPTS=$(printf '%%s' "$JAVA_OPTS" | tr '\n' ' ') # Start building new JAVA_OPTS JAVA_OPTS="" +# Escape replacement-special chars once; these values are loop-invariant. +_escaped_deps_dir="${DEPS_DIR//\\/\\\\}" +_escaped_deps_dir="${_escaped_deps_dir//&/\\&}" +_escaped_home="${HOME//\\/\\\\}" +_escaped_home="${_escaped_home//&/\\&}" +_user_java_opts_placeholder='__JAVA_OPTS_BUILDPACK_PLACEHOLDER__' +_escaped_user_java_opts="${USER_JAVA_OPTS//\\/\\\\}" +_escaped_user_java_opts="${_escaped_user_java_opts//&/\\&}" + if [ -d "$DEPS_DIR/%s/java_opts" ]; then for opts_file in "$DEPS_DIR/%s/java_opts"/*.opts; do if [ -f "$opts_file" ]; then @@ -87,15 +96,14 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then opts_content=$(cat "$opts_file") # Expand $DEPS_DIR and $HOME using bash parameter expansion. - # sed-based substitution breaks when these values contain the sed delimiter (|), - # backslashes, ampersands, or newlines — all valid in JAVA_OPTS and paths. - opts_content="${opts_content//\$DEPS_DIR/$DEPS_DIR}" - opts_content="${opts_content//\$HOME/$HOME}" + # In ${var//pattern/repl}, '&' and '\' are special in replacement strings, + # so escape them first to preserve literal path contents. + opts_content="${opts_content//\$DEPS_DIR/$_escaped_deps_dir}" + opts_content="${opts_content//\$HOME/$_escaped_home}" # Shield $JAVA_OPTS from eval: replace with a placeholder first, # then substitute the actual value AFTER eval so that quotes and # backslashes in the user-provided JAVA_OPTS are never exposed to eval. - _user_java_opts_placeholder='__JAVA_OPTS_BUILDPACK_PLACEHOLDER__' opts_content="${opts_content//\$JAVA_OPTS/$_user_java_opts_placeholder}" # Expand any remaining environment variables in opts content via eval. @@ -105,8 +113,6 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then opts_content=$(eval "printf '%%s' \"$opts_content\"") # Now safely substitute JAVA_OPTS after eval (preserves quotes, backslashes, and ampersands) - _escaped_user_java_opts="${USER_JAVA_OPTS//\\/\\\\}" - _escaped_user_java_opts="${_escaped_user_java_opts//&/\\&}" opts_content="${opts_content//$_user_java_opts_placeholder/$_escaped_user_java_opts}" if [ -n "$opts_content" ]; then diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 86dcb3f5c..5a648d53d 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -77,6 +77,17 @@ var _ = Describe("Java Opts Writer", func() { return string(output), err } + runWithCustomRuntimeEnv := func(scriptPath, javaOpts, bashExpr, runtimeDepsDir, runtimeHome string) (string, error) { + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && "+bashExpr) + cmd.Env = append(os.Environ(), + "JAVA_OPTS="+javaOpts, + "DEPS_DIR="+runtimeDepsDir, + "HOME="+runtimeHome, + ) + output, err := cmd.CombinedOutput() + return string(output), err + } + runScript := func(javaOpts string, optsFileContent string) (string, error) { scriptPath := setupScript(javaOpts, optsFileContent) return runWithEnv(scriptPath, javaOpts, `printf '%s\n' "$JAVA_OPTS"`) @@ -123,6 +134,14 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring("-javaagent:/home/vcap/app/BOOT-INF/lib/agent.jar")) }) + It("preserves ampersand and backslash in $HOME replacement", func() { + scriptPath := setupScript("", "-javaagent:$HOME/BOOT-INF/lib/agent.jar") + customHome := `/tmp/home&dir\sub` + output, err := runWithCustomRuntimeEnv(scriptPath, "", `printf '%s\n' "$JAVA_OPTS"`, depsDir, customHome) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-javaagent:" + customHome + "/BOOT-INF/lib/agent.jar")) + }) + It("expands $DEPS_DIR in opts file content", func() { output, err := runScript("", "-Djava.security.properties=$DEPS_DIR/0/security.properties") Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) @@ -187,6 +206,12 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring(`-Dpath=C:\tmp\app`)) }) + It("preserves double backslashes in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dpath=C:\\double`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpath=C:\\double`)) + }) + // Full invocation cycle test for issue #1301: // Verifies that the quoted eval "exec ... $JAVA_OPTS" form delivers the correct // argument to java — glob chars in $JAVA_OPTS are not expanded. From edbf42d3027cfd0d5843a93931e5b92f3274cee9 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Fri, 5 Jun 2026 17:41:38 +0000 Subject: [PATCH 7/9] fix(play): quote eval java command for JAVA_OPTS safety (#1301) --- src/java/containers/play.go | 4 ++-- src/java/containers/play_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java/containers/play.go b/src/java/containers/play.go index 2dc37717a..3001c6d7b 100644 --- a/src/java/containers/play.go +++ b/src/java/containers/play.go @@ -548,8 +548,8 @@ func (p *PlayContainer) Release() (string, error) { libPath = filepath.ToSlash(relPath) } } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - cmd = fmt.Sprintf("eval exec java $JAVA_OPTS -cp $HOME/%s/* play.core.server.NettyServer $HOME", libPath) + // Use quoted eval argument to prevent glob/word-splitting of $JAVA_OPTS before eval. + cmd = fmt.Sprintf(`eval "exec java $JAVA_OPTS -cp $HOME/%s/* play.core.server.NettyServer $HOME"`, libPath) } p.context.Log.Debug("Play Framework release command: %s", cmd) diff --git a/src/java/containers/play_test.go b/src/java/containers/play_test.go index d4d225434..6d758afe3 100644 --- a/src/java/containers/play_test.go +++ b/src/java/containers/play_test.go @@ -185,7 +185,7 @@ var _ = Describe("Play Container", func() { It("returns correct java command", func() { cmd, err := container.Release() Expect(err).NotTo(HaveOccurred()) - Expect(cmd).To(ContainSubstring("java $JAVA_OPTS -cp")) + Expect(cmd).To(ContainSubstring(`eval "exec java $JAVA_OPTS -cp`)) Expect(cmd).To(ContainSubstring("play.core.server.NettyServer $HOME")) }) }) @@ -201,7 +201,7 @@ var _ = Describe("Play Container", func() { It("returns correct java command", func() { cmd, err := container.Release() Expect(err).NotTo(HaveOccurred()) - Expect(cmd).To(ContainSubstring("java $JAVA_OPTS -cp")) + Expect(cmd).To(ContainSubstring(`eval "exec java $JAVA_OPTS -cp`)) Expect(cmd).To(ContainSubstring("play.core.server.NettyServer $HOME")) }) }) From afa8f9d99c181a3a1f799892ca213ce462481684 Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 8 Jun 2026 12:49:48 +0000 Subject: [PATCH 8/9] fix: escape double quotes in JBP_CONFIG_JAVA_MAIN arguments; rename unused test param Arguments from JBP_CONFIG_JAVA_MAIN containing double quotes would break the outer eval "..." string in the start command. Escape them before building the javaArgs string. Add regression test. Rename unused 'javaOpts' param in setupScript test helper to '_' to document that it has no effect on script generation. --- src/java/containers/java_main.go | 3 ++- src/java/containers/java_main_test.go | 14 ++++++++++++++ src/java/frameworks/java_opts_writer_test.go | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index 374c8250a..08b6ceb55 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -286,7 +286,8 @@ func (j *JavaMainContainer) Release() (string, error) { args := "" if cfg.Arguments != "" { - args = " " + cfg.Arguments + // Escape double quotes so they don't terminate the outer eval "..." string. + args = " " + strings.ReplaceAll(cfg.Arguments, `"`, `\"`) } // JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class. diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index ebd094507..a4dec30d5 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -282,6 +282,20 @@ var _ = Describe("Java Main Container", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmd).To(ContainSubstring("--foo=bar")) }) + + It("escapes double quotes in arguments to avoid breaking the eval string", func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{arguments: "--spring.datasource.url=\"jdbc:h2:mem:test\""}`) + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + // Double quotes in arguments must be backslash-escaped so they + // don't terminate the outer eval "..." string. + Expect(cmd).To(ContainSubstring(`--spring.datasource.url=\"jdbc:h2:mem:test\"`)) + }) }) // Regression tests for issue #1301: start command must use eval "exec ... $JAVA_OPTS" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index 5a648d53d..d2084c8f6 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -55,7 +55,7 @@ var _ = Describe("Java Opts Writer", func() { }) Describe("CreateJavaOptsAssemblyScript", func() { - setupScript := func(javaOpts string, optsFileContent string) string { + setupScript := func(_ string, optsFileContent string) string { err := frameworks.CreateJavaOptsAssemblyScript(ctx) Expect(err).NotTo(HaveOccurred()) From b277fabdd71362fa386fd177890cebd2dc79215c Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Mon, 8 Jun 2026 13:21:02 +0000 Subject: [PATCH 9/9] fix(java_opts): escape " and \ in opts_content before eval expansion eval "printf '%s' \"\$opts_content\"" mangles .opts content containing literal double quotes (e.g. Datadog writes -Ddd.service="myapp"): the inner " terminates the outer eval string and the value is stripped. Escape \ (first) and " in opts_content into a temporary _eval_safe variable before expanding it into the eval command, so both chars are preserved. Add regression tests for double-quoted and backslash .opts values. --- src/java/frameworks/java_opts_writer.go | 7 ++++++- src/java/frameworks/java_opts_writer_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index f45a742a8..66db45f8b 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -106,11 +106,16 @@ if [ -d "$DEPS_DIR/%s/java_opts" ]; then # backslashes in the user-provided JAVA_OPTS are never exposed to eval. opts_content="${opts_content//\$JAVA_OPTS/$_user_java_opts_placeholder}" + # Escape \ and " in opts_content so they do not break the + # eval "..." wrapper below. \ must be escaped first. + _eval_safe="${opts_content//\\/\\\\}" + _eval_safe="${_eval_safe//\"/\\\"}" + # Expand any remaining environment variables in opts content via eval. # Note: eval executes commands, but .opts files are written by the buildpack # at staging time and run within the container context. # This matches how the Ruby buildpack naturally expanded variables via shell. - opts_content=$(eval "printf '%%s' \"$opts_content\"") + opts_content=$(eval "printf '%%s' \"$_eval_safe\"") # Now safely substitute JAVA_OPTS after eval (preserves quotes, backslashes, and ampersands) opts_content="${opts_content//$_user_java_opts_placeholder/$_escaped_user_java_opts}" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index d2084c8f6..b97f12f57 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -221,5 +221,20 @@ var _ = Describe("Java Opts Writer", func() { // Java receives exactly one arg: -DcronSched=0 */7 * * * * Expect(strings.TrimSpace(output)).To(Equal("-DcronSched=0 */7 * * * *")) }) + + // Regression test: eval mangles .opts content containing literal double quotes. + // e.g. Datadog writes -Ddd.service="myapp" into its .opts file; the inner " + // terminates the outer eval "..." string, stripping the value. + It("preserves double-quoted values from .opts file content", func() { + output, err := runScript("", `-Ddd.service="myapp"`) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Ddd.service="myapp"`)) + }) + + It("preserves backslashes in .opts file content", func() { + output, err := runScript("", `-Dpattern=foo\|bar`) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpattern=foo\|bar`)) + }) }) })