diff --git a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerQuery.java b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerQuery.java index 326b4ae0eb..1c068f7682 100644 --- a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerQuery.java +++ b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerQuery.java @@ -181,7 +181,19 @@ public String getFcliCmdArgs(Map toolArgs) { } private final void addQuery(ArrayList queries, String schemaPropertyName, String value) { - var fieldName = fieldsBySchemaPropertyName.getOrDefault(schemaPropertyName, schemaPropertyName); - queries.add(String.format("%s matches '%s'", fieldName, value)); + // Validate that schemaPropertyName is in the known set of safe fields (do not fall back to attacker-supplied name) + if ( !fieldsBySchemaPropertyName.containsKey(schemaPropertyName) ) { + throw new FcliSimpleException("Unknown query field '%s'; allowed fields are: %s", + schemaPropertyName, String.join(", ", fieldsBySchemaPropertyName.keySet())); + } + var fieldName = fieldsBySchemaPropertyName.get(schemaPropertyName); + // Escape backslashes first, then single quotes to prevent SpEL string literal breakout + var escapedValue = value.replace("\\", "\\\\") + .replace("'", "\\'"); + // Reject values containing unescaped double quotes or certain problematic characters that could break CLI token boundaries + if ( value.contains("\"") ) { + throw new FcliSimpleException("Query value contains unescaped double quote which is not allowed for CLI safety"); + } + queries.add(String.format("%s matches '%s'", fieldName, escapedValue)); } } \ No newline at end of file diff --git a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/output/writer/record/impl/RecordWriterTable.java b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/output/writer/record/impl/RecordWriterTable.java index 8ae3a71635..25d3343403 100644 --- a/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/output/writer/record/impl/RecordWriterTable.java +++ b/fcli-core/fcli-common-core/src/main/java/com/fortify/cli/common/output/writer/record/impl/RecordWriterTable.java @@ -96,9 +96,27 @@ private String getColumnValue(ObjectNode formattedRecord, String property) { var node = formattedRecord.get(property); if ( node==null || node.isNull() ) { return "N/A"; } if ( node.isArray() ) { - return JsonHelper.stream((ArrayNode)node).map(n->n.asText()).collect(Collectors.joining(",")); + return JsonHelper.stream((ArrayNode)node).map(n->stripAnsiAndControlChars(n.asText())).collect(Collectors.joining(",")); } - return node.asText(); + return stripAnsiAndControlChars(node.asText()); + } + + /** + * Strip ANSI escape sequences and control characters from string to prevent terminal injection attacks. + * Preserves newlines and tabs as they may be used intentionally in table layout. + * Removes: ESC (0x1B) and C0 control chars (0x00-0x1F) except TAB (0x09) and LF (0x0A). + */ + private String stripAnsiAndControlChars(String input) { + if ( input == null || input.isEmpty() ) { return input; } + // Remove ANSI escape sequences and C0 control chars, preserving TAB and LF + StringBuilder result = new StringBuilder(); + for ( char c : input.toCharArray() ) { + // Keep printable chars and common whitespace (TAB, LF); skip ESC and other control chars + if ( (c >= 0x20 && c <= 0x7E) || c == 0x09 || c == 0x0A ) { + result.append(c); + } + } + return result.toString(); } @Override diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserCreateLocalCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserCreateLocalCommand.java index 053e0d3500..f8a65a4dee 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserCreateLocalCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserCreateLocalCommand.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.cli.util.CommandGroup; +import com.fortify.cli.common.log.LogSensitivityLevel; +import com.fortify.cli.common.log.MaskValue; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; import com.fortify.cli.ssc._common.rest.ssc.SSCUrls; @@ -38,7 +40,8 @@ public class SSCUserCreateLocalCommand extends AbstractSSCJsonNodeOutputCommand @Option(names = {"--username"}, required = true) private String username; - @Option(names = {"--password"}, required = true) + @Option(names = {"--password"}, required = true, interactive = true, echo = false, arity = "0..1") + @MaskValue(sensitivity = LogSensitivityLevel.high, description = "PASSWORD") private String password; @Option(names = {"--firstname"}) private String firstName; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserUpdateLocalCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserUpdateLocalCommand.java index f1baafd9bd..cf137bf845 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserUpdateLocalCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/access_control/cli/cmd/SSCUserUpdateLocalCommand.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.cli.util.CommandGroup; import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.log.LogSensitivityLevel; +import com.fortify.cli.common.log.MaskValue; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; import com.fortify.cli.ssc._common.output.cli.cmd.AbstractSSCJsonNodeOutputCommand; @@ -47,7 +49,8 @@ public class SSCUserUpdateLocalCommand extends AbstractSSCJsonNodeOutputCommand private String lastName; @Option(names = {"--email"}) private String email; - @Option(names = {"--password"}) + @Option(names = {"--password"}, interactive = true, echo = false, arity = "0..1") + @MaskValue(sensitivity = LogSensitivityLevel.high, description = "PASSWORD") private String password; @Option(names = {"--password-never-expires", "--pne"}) private Boolean pwNeverExpires; diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/helper/SSCActionSpelFunctions.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/helper/SSCActionSpelFunctions.java index 23d51622d4..a50d2f73c3 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/helper/SSCActionSpelFunctions.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/action/helper/SSCActionSpelFunctions.java @@ -21,6 +21,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import javax.xml.XMLConstants; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; @@ -174,7 +175,9 @@ private final String processFpr(RawResponse r, Function consu private final void processAuditFvdl(InputStream is, Function consumer) throws XMLStreamException { var factory = XMLInputFactory.newInstance(); - factory.setXMLResolver(null); // Prevent XML External Entity Injection + // Disable DTD processing and external entity resolution to prevent XXE attacks + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); var reader = factory.createXMLStreamReader(is); while(reader.hasNext()) { int eventType = reader.next();