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 @@ -224,7 +224,37 @@ private boolean isSafetyBoundaryQuery(String question) {
compact.contains("api키") ||
compact.contains("토큰") ||
compact.contains("비밀번호") ||
compact.contains("패스워드");
compact.contains("패스워드") ||
isOperationalCommandQuery(normalized, compact);
}

private boolean isOperationalCommandQuery(String normalized, String compact) {
if (normalized == null || normalized.isBlank()) {
return false;
}

if (compact.contains("../") ||
compact.contains("..\\") ||
compact.contains("/etc/") ||
compact.contains("/proc/") ||
compact.contains("/var/log") ||
compact.contains("~/.ssh") ||
compact.contains("idrsa") ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The check compact.contains("idrsa") will not match the standard SSH private key filename id_rsa because compact is created by removing spaces (normalized.replace(" ", "")), which preserves underscores. To ensure robust blocking of private key exposure attempts, please add a check for id_rsa as well.

            compact.contains("id_rsa") ||
            compact.contains("idrsa") ||

compact.contains(".env")) {
return true;
}

if (normalized.contains("&&") ||
normalized.contains("||") ||
normalized.contains("$(") ||
normalized.contains("`") ||
normalized.contains(" >") ||
normalized.contains(" <") ||
normalized.contains(" | ")) {
return true;
}

return normalized.matches("^(cd|ls|ll|pwd|cat|env|printenv|whoami|id|uname|hostname|ps|top|htop|free|df|du|docker|kubectl|curl|wget|ssh|scp|sudo|su|rm|cp|mv|chmod|chown|find|grep|tail|head|less|vi|vim|nano)(\\s+.*)?$");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two major issues with this regular expression check:

  1. High False-Positive Rate: The regex matches any query starting with common English words like free, find, top, less, cat, or id (e.g., "free tickets", "find events near me", "top 10 events", "less than 10000 won"). Since this is an event and ticketing platform, these are highly legitimate user queries that will be incorrectly blocked.
  2. Performance Overhead: String.matches() compiles the regular expression pattern on every single invocation, which introduces significant CPU overhead under high traffic.

Recommendations:

  • Remove highly common English words (free, find, top, less, cat, id, env) from the prefix matching list, or only block them if they are accompanied by typical shell arguments/flags (e.g., free -m, find .).
  • Precompile the regular expression as a private static final Pattern at the class level (e.g., OPERATIONAL_COMMAND_PATTERN).
Suggested change
return normalized.matches("^(cd|ls|ll|pwd|cat|env|printenv|whoami|id|uname|hostname|ps|top|htop|free|df|du|docker|kubectl|curl|wget|ssh|scp|sudo|su|rm|cp|mv|chmod|chown|find|grep|tail|head|less|vi|vim|nano)(\\s+.*)?$");
return OPERATIONAL_COMMAND_PATTERN.matcher(normalized).matches();

}

private boolean isEventInformationQuery(String question) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,25 @@ void promptInjectionRequestIsBlockedBeforeSearchOrLlm() throws Exception {
verify(llmRouter, never()).pick(any());
}

@Test
void operationalCommandRequestIsBlockedBeforeSearchOrLlm() throws Exception {
RagChatService.RagResponse response = ragChatService.chat(
"cd ../",
List.of(ChatMessageDto.user("cd ../")),
10L
);

assertThat(response.isHasContext()).isFalse();
assertThat(response.getAnswer())
.contains("도와드릴 수 없어요")
.contains("서버 자원");
verify(vectorSearchService, never()).searchUserData(any(), any());
verify(vectorSearchService, never()).searchUserPrivate(any(), any());
verify(vectorSearchService, never()).searchPublicOnly(any());
verify(vectorSearchService, never()).searchPublicEventsFirst(any());
verify(llmRouter, never()).pick(any());
}

@Test
void questionWithoutFairPlayContextDoesNotCallLlm() throws Exception {
when(vectorSearchService.searchPublicOnly("미국 수도가 어디야?"))
Expand Down
Loading