Skip to content

Commit 61640a0

Browse files
authored
Merge pull request #4244 from 1c-syntax/claude/spring-boot-debug-guard-ovygse
fix(cli): guard against accidental Spring Boot debug mode
2 parents 0bd6d38 + 3e03772 commit 61640a0

3 files changed

Lines changed: 163 additions & 3 deletions

File tree

src/main/java/com/github/_1c_syntax/bsl/languageserver/MainApplication.java

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import java.util.List;
4848
import java.util.Set;
4949
import java.util.concurrent.Callable;
50+
import java.util.function.Predicate;
5051
import java.util.regex.Pattern;
5152

5253
import static picocli.CommandLine.Command;
@@ -84,6 +85,23 @@ public class MainApplication implements Callable<Integer>, ExitCodeGenerator {
8485

8586
private static final String DEFAULT_COMMAND = "lsp";
8687

88+
/**
89+
* Опция осознанного включения отладочного режима Spring Boot.
90+
* <p>
91+
* Без неё любой запрос на отладку — флаги {@code --debug}/{@code --trace} или переменные среды
92+
* {@code DEBUG}/{@code TRACE} (Spring привязывает их к свойствам {@code debug}/{@code trace}
93+
* по relaxed binding) — игнорируется: см. {@link #guardSpringDebugMode(String[])}.
94+
*/
95+
static final String ENABLE_DEBUG_OPTION = "--enable-debug-i-know-what-i-am-doing";
96+
97+
/** {@link #ENABLE_DEBUG_OPTION} с необязательным значением; группа 1 — значение ({@code null}, если его нет). */
98+
private static final Pattern ENABLE_DEBUG_OPTION_PATTERN =
99+
CaseInsensitivePattern.compile(Pattern.quote(ENABLE_DEBUG_OPTION) + "(?:=(.*))?");
100+
101+
/** Отладочные флаги Spring Boot: {@code --debug}/{@code --trace} с необязательным {@code =value}. */
102+
private static final Pattern DEBUG_TRACE_FLAG_PATTERN =
103+
CaseInsensitivePattern.compile("--(?:debug|trace)(?:=.*)?");
104+
87105
@Option(
88106
names = {"-h", "--help"},
89107
usageHelp = true,
@@ -107,14 +125,16 @@ public class MainApplication implements Callable<Integer>, ExitCodeGenerator {
107125
CaseInsensitivePattern.compile("--server\\..*"),
108126
// Опции команды по умолчанию (lsp), допустимые без явного указания команды: `--mcp`, `--mcp-path`.
109127
CaseInsensitivePattern.compile("--mcp(-path)?(=.*)?"),
110-
CaseInsensitivePattern.compile("--debug")
128+
DEBUG_TRACE_FLAG_PATTERN,
129+
ENABLE_DEBUG_OPTION_PATTERN
111130
);
112131

113132
private final CommandLine.IFactory picocliFactory;
114133

115134
private int exitCode;
116135

117136
public static void main(String[] args) {
137+
args = guardSpringDebugMode(args);
118138
applyMcpEndpointPath(args);
119139

120140
var applicationContext = new SpringApplicationBuilder(MainApplication.class)
@@ -253,6 +273,58 @@ private static boolean isMcpSubcommandOverHttp(String[] args) {
253273
return protocol.equals("sse") || protocol.equals("streamable");
254274
}
255275

276+
/**
277+
* Защита от случайного включения отладочного режима Spring Boot.
278+
* <p>
279+
* Отладка ({@code --debug}/{@code --trace}, свойства {@code debug}/{@code trace} или переменные
280+
* среды {@code DEBUG}/{@code TRACE}) включает объёмный лог core-логгеров и отчёт об
281+
* автоконфигурации. Для LSP по stdio это особенно опасно: посторонний вывод способен замусорить
282+
* канал протокола. При этом {@code DEBUG} — распространённое имя переменной среды, её легко
283+
* выставить в окружении случайно, а переименовать/отключить ключ на уровне Spring Boot нельзя.
284+
* <p>
285+
* Поэтому при явной передаче {@link #ENABLE_DEBUG_OPTION} аргументы отдаются как есть. Иначе
286+
* отладка гасится до старта контекста: свойства {@code debug}/{@code trace} принудительно
287+
* выставляются в {@code false} (системное свойство приоритетнее переменной среды, поэтому случайно
288+
* заданный {@code DEBUG}/{@code TRACE} тоже не сработает), а флаги {@code --debug}/{@code --trace}
289+
* убираются из аргументов.
290+
*
291+
* @param args исходные аргументы командной строки
292+
* @return исходные аргументы при осознанном включении; иначе — аргументы без флагов
293+
* {@code --debug}/{@code --trace}
294+
*/
295+
static String[] guardSpringDebugMode(String[] args) {
296+
if (hasDebugOptIn(args)) {
297+
// Пользователь осознанно включил отладку — аргументы не трогаем.
298+
return args;
299+
}
300+
301+
System.setProperty("debug", "false");
302+
System.setProperty("trace", "false");
303+
304+
var cleaned = removeArgs(args, arg -> DEBUG_TRACE_FLAG_PATTERN.matcher(arg).matches());
305+
if (cleaned.length != args.length) {
306+
System.err.println(
307+
"Ignoring --debug/--trace: verbose Spring Boot debug logging can corrupt the LSP stdout "
308+
+ "channel. Pass " + ENABLE_DEBUG_OPTION + "=true to enable it intentionally.");
309+
}
310+
return cleaned;
311+
}
312+
313+
private static boolean hasDebugOptIn(String[] args) {
314+
for (var arg : args) {
315+
var matcher = ENABLE_DEBUG_OPTION_PATTERN.matcher(arg);
316+
// Голый флаг (значение null) и =true включают отладку; =false — нет.
317+
if (matcher.matches() && !"false".equalsIgnoreCase(matcher.group(1))) {
318+
return true;
319+
}
320+
}
321+
return false;
322+
}
323+
324+
private static String[] removeArgs(String[] args, Predicate<String> drop) {
325+
return Arrays.stream(args).filter(drop.negate()).toArray(String[]::new);
326+
}
327+
256328
/**
257329
* Перенести значение {@code --mcp-path} в системное свойство до старта контекста:
258330
* эндпоинт Streamable HTTP регистрируется автоконфигурацией на refresh, раньше выполнения команды.

src/test/java/com/github/_1c_syntax/bsl/languageserver/MainApplicationModeTest.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,91 @@ void webApplicationTypePerMode() {
6464
.isEqualTo(WebApplicationType.SERVLET);
6565
}
6666

67+
@Test
68+
void debugFlagsStrippedWithoutOptIn() {
69+
System.clearProperty("debug");
70+
System.clearProperty("trace");
71+
try {
72+
var result = MainApplication.guardSpringDebugMode(new String[]{"analyze", "--debug", "--trace"});
73+
74+
assertThat(result).containsExactly("analyze");
75+
assertThat(System.getProperty("debug")).isEqualTo("false");
76+
assertThat(System.getProperty("trace")).isEqualTo("false");
77+
} finally {
78+
System.clearProperty("debug");
79+
System.clearProperty("trace");
80+
}
81+
}
82+
83+
@Test
84+
void debugFlagsKeptWithOptIn() {
85+
System.clearProperty("debug");
86+
System.clearProperty("trace");
87+
try {
88+
var result = MainApplication.guardSpringDebugMode(
89+
new String[]{"analyze", "--debug", MainApplication.ENABLE_DEBUG_OPTION + "=true"});
90+
91+
// При осознанном включении аргументы отдаются как есть, свойства не трогаются.
92+
assertThat(result)
93+
.containsExactly("analyze", "--debug", MainApplication.ENABLE_DEBUG_OPTION + "=true");
94+
assertThat(System.getProperty("debug")).isNull();
95+
} finally {
96+
System.clearProperty("debug");
97+
System.clearProperty("trace");
98+
}
99+
}
100+
101+
@Test
102+
void optInWithFalseValueDoesNotEnableDebug() {
103+
System.clearProperty("debug");
104+
System.clearProperty("trace");
105+
try {
106+
var result = MainApplication.guardSpringDebugMode(
107+
new String[]{"--debug", MainApplication.ENABLE_DEBUG_OPTION + "=false"});
108+
109+
// =false не включает отладку: флаг --debug снят, свойство debug принудительно false.
110+
assertThat(result).containsExactly(MainApplication.ENABLE_DEBUG_OPTION + "=false");
111+
assertThat(System.getProperty("debug")).isEqualTo("false");
112+
} finally {
113+
System.clearProperty("debug");
114+
System.clearProperty("trace");
115+
}
116+
}
117+
118+
@Test
119+
void nonDebugArgsPassThroughWithDebugForcedOff() {
120+
System.clearProperty("debug");
121+
System.clearProperty("trace");
122+
try {
123+
var result = MainApplication.guardSpringDebugMode(new String[]{"analyze", "--srcDir", "."});
124+
125+
// Обычные аргументы не меняются, но без опт-ина отладка всё равно гасится.
126+
assertThat(result).containsExactly("analyze", "--srcDir", ".");
127+
assertThat(System.getProperty("debug")).isEqualTo("false");
128+
assertThat(System.getProperty("trace")).isEqualTo("false");
129+
} finally {
130+
System.clearProperty("debug");
131+
System.clearProperty("trace");
132+
}
133+
}
134+
135+
@Test
136+
void debugFromSystemPropertyForcedOff() {
137+
// Запрос на отладку без флага командной строки — через свойство debug (как relaxed-binding DEBUG).
138+
System.clearProperty("debug");
139+
System.clearProperty("trace");
140+
try {
141+
System.setProperty("debug", "true");
142+
var result = MainApplication.guardSpringDebugMode(new String[]{"analyze"});
143+
144+
assertThat(result).containsExactly("analyze");
145+
assertThat(System.getProperty("debug")).isEqualTo("false");
146+
} finally {
147+
System.clearProperty("debug");
148+
System.clearProperty("trace");
149+
}
150+
}
151+
67152
@Test
68153
void mcpEndpointPathAppliedOnlyForMcpHttp() {
69154
System.clearProperty(MCP_ENDPOINT_PROPERTY);

src/test/java/com/github/_1c_syntax/bsl/languageserver/architecture/ArchitectureTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,18 @@ class ArchitectureTest {
110110
// --- Стандартные потоки -------------------------------------------------------------------------
111111
// Никто не пишет в стандартные потоки stdout и stderr. Исключения — лишь места, где стандартный
112112
// поток нужен по протоколу или природе процесса. Это транспорт LSP в классе
113-
// LanguageServerLauncherConfiguration, транспорт MCP по stdio в классе McpStdioConfiguration и
113+
// LanguageServerLauncherConfiguration, транспорт MCP по stdio в классе McpStdioConfiguration,
114114
// аварийный fallback в классе ParentProcessWatcher на завершении процесса, когда логгер уже
115-
// недоступен. Новый класс с такой потребностью добавляется в список исключений осознанно, через ревью.
115+
// недоступен, и гвард отладочного режима в MainApplication, срабатывающий в main() ещё до
116+
// конфигурации логирования (в stderr, чтобы не задеть stdout-канал LSP). Новый класс с такой
117+
// потребностью добавляется в список исключений осознанно, через ревью.
116118

117119
@ArchTest
118120
static final ArchRule no_classes_should_access_standard_streams = noClasses()
119121
.that().doNotHaveFullyQualifiedName(ROOT_PACKAGE + ".cli.lsp.LanguageServerLauncherConfiguration")
120122
.and().doNotHaveFullyQualifiedName(ROOT_PACKAGE + ".mcp.McpStdioConfiguration")
121123
.and().doNotHaveFullyQualifiedName(ROOT_PACKAGE + ".lsp.ParentProcessWatcher")
124+
.and().doNotHaveFullyQualifiedName(ROOT_PACKAGE + ".MainApplication")
122125
.should(ACCESS_STANDARD_STREAMS)
123126
.because("вывод в стандартные потоки допустим только в транспортных точках и аварийном "
124127
+ "fallback из списка выше; остальной код пишет через slf4j");

0 commit comments

Comments
 (0)