From c2ea9cc40319675766808d3136cb452c2b7f32d8 Mon Sep 17 00:00:00 2001 From: Madhur Jain <87946372+jmadhur87@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:19:35 +0530 Subject: [PATCH 1/2] chore: Update int to Long_Int (#1081) --- .../cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java index 1ec99fa9144..ac8a70ae2bb 100644 --- a/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java +++ b/fcli-core/fcli-ssc/src/main/java/com/fortify/cli/ssc/issue_template/cli/cmd/AbstractSSCIssueTemplateUpdateCommand.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fortify.cli.common.json.JsonHelper; import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; @@ -63,7 +63,7 @@ protected ArrayNode getCustomTagIds(UnirestInstance unirest, SSCIssueTemplateDes currentTags, addTagsMixin.getTagSpecs(), rmTagsMixin.getTagSpecs() - ).map(tag -> new IntNode(Integer.valueOf(tag.getId()))).collect(JsonHelper.arrayNodeCollector()); + ).map(tag -> new LongNode(Long.valueOf(tag.getId()))).collect(JsonHelper.arrayNodeCollector()); } @Override From 778b91937f18f3ff6ba03b5763a55d7808ba2e5e Mon Sep 17 00:00:00 2001 From: mjain6 Date: Wed, 16 Sep 2026 22:14:54 +0530 Subject: [PATCH 2/2] Fix FAA identified Vuln --- .../arg/MCPToolArgHandlerActionOption.java | 16 +++++++++- .../cli/aviator/fpr/utils/FileUtils.java | 13 +++++++++ .../fortify/cli/aviator/util/Constants.java | 3 ++ .../ssc/cli/cmd/AviatorSSCAuditCommand.java | 29 +++++++++++++++++-- .../helper/DastFprCorrelationEnricher.java | 11 +++++++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerActionOption.java b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerActionOption.java index 059de4fa1c3..820744522d1 100644 --- a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerActionOption.java +++ b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/arg/MCPToolArgHandlerActionOption.java @@ -59,7 +59,21 @@ public String getFcliCmdArgs(Map toolArgs) { if ( values.isEmpty() ) { return ""; } - return String.format("\"%s=%s\"", name, String.join(",", values)); + // Escape embedded quotes to prevent command injection via quote breakout + var escapedValues = values.stream() + .map(MCPToolArgHandlerActionOption::escapeQuotes) + .toList(); + return String.format("\"%s=%s\"", name, String.join(",", escapedValues)); + } + + /** + * Escapes embedded double quotes by prefixing with backslash. + * Prevents command injection when this value is used in a quoted command string. + * @param value The unescaped value + * @return The value with embedded quotes escaped (e.g., " becomes \") + */ + private static String escapeQuotes(String value) { + return value.replace("\"", "\\\""); } private static Stream streamValueElements(Object value) { diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java index f5878cc62d8..cfa0d4e41a2 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/fpr/utils/FileUtils.java @@ -29,6 +29,7 @@ import com.fortify.cli.aviator.audit.model.Fragment; import com.fortify.cli.aviator.fpr.model.FVDLMetadata; +import com.fortify.cli.aviator.util.Constants; import com.fortify.cli.aviator.util.FileTypeLanguageMapperUtil; import com.fortify.cli.aviator.util.FileUtil; import com.fortify.cli.aviator.util.FprHandle; @@ -68,6 +69,12 @@ public List readFileWithFallback(Path filePath) { private List readFileWithFallback(Path filePath, String filename) { return fileContentCache.computeIfAbsent(filePath, path -> { try { + long fileSize = Files.size(path); + if (fileSize > Constants.MAX_SOURCE_FILE_SIZE) { + logger.warn("Source file exceeds maximum allowed size ({} bytes): {} (actual size: {} bytes)", + Constants.MAX_SOURCE_FILE_SIZE, path, fileSize); + return Collections.emptyList(); + } byte[] fileBytes = Files.readAllBytes(path); String content = sourceDecoder.decode(fileBytes, filename, fvdlMetadata).content(); return Arrays.asList(content.split("\\r?\\n")); @@ -163,6 +170,12 @@ String readSourceFileContentStrict(FprHandle fprHandle, String relativePath) thr throw new IOException("Source file key not found in sourceFileMap: " + relativePath); } + long fileSize = Files.size(actualSourcePath); + if (fileSize > Constants.MAX_SOURCE_FILE_SIZE) { + throw new IOException("Source file exceeds maximum allowed size (" + Constants.MAX_SOURCE_FILE_SIZE + + " bytes): " + relativePath + " (actual size: " + fileSize + " bytes)"); + } + byte[] fileBytes = Files.readAllBytes(actualSourcePath); return sourceDecoder.decode(fileBytes, relativePath, fvdlMetadata).content(); } diff --git a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java index 00613cd242b..15fdf606f1d 100644 --- a/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java +++ b/fcli-core/fcli-aviator-common/src/main/java/com/fortify/cli/aviator/util/Constants.java @@ -73,6 +73,9 @@ public static boolean isAviatorAuditUsername(String username) { public static final String MAX_PER_CATEGORY_EXCEEDED = "Fortify detected {issues_new_in_category} new issues in this (sub)category. Fortify Remediation Aviator auditing was limited to the first {MAX_PER_CATEGORY}."; public static final String MAX_TOTAL_EXCEEDED = "Fortify detected {issues_new_total} new issues. Fortify Remediation Aviator auditing was limited to {MAX_TOTAL} issues in total, while ensuring that representative issues in each category were audited."; + // File size protection — prevent zip bomb and decompression DOS attacks + public static final long MAX_SOURCE_FILE_SIZE = 50L * 1024L * 1024L; // 50 MB + // Operation constants for error messages public static final String OP_CREATE_APP = "application creation"; public static final String OP_ADD_APP_ENTITLEMENT = "application entitlement increment"; diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java index f0c56048b13..2f54cf21b4e 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/cli/cmd/AviatorSSCAuditCommand.java @@ -165,6 +165,7 @@ private List getFolderPriorityOrder() { /** * Checks quota constraints when --skip-if-exceeding-quota or --test-exceeding-quota is active. + * Fails closed when quota cannot be reliably determined and user requested quota protection. * @return a result JsonNode if the audit should be skipped/reported, or null if the audit should proceed. */ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, @@ -189,8 +190,16 @@ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, } } - // If auditable issue count is unknown (-1), skip quota comparison and proceed with audit + // If auditable issue count is unknown (-1), fail closed when user requested quota protection if (auditableIssueCount < 0) { + if (isSkipIfExceedingQuota()) { + LOG.warn("Auditable issue count unknown; cannot honor --skip-if-exceeding-quota for {}:{}. Audit skipped.", + av.getApplicationName(), av.getVersionName()); + ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED"); + AviatorSSCAuditHelper.setOperationMessage(result, + "Cannot determine issue count; audit skipped per --skip-if-exceeding-quota"); + return result; + } LOG.info("Auditable issue count unknown; skipping quota evaluation for {}:{}.", av.getApplicationName(), av.getVersionName()); return null; @@ -201,6 +210,7 @@ private JsonNode checkQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, /** * Handles the case where the application is not found in Aviator. + * Fails closed when default quota cannot be determined and quota protection is enabled. * @return the resolved quota (possibly from default), or QUOTA_APP_NOT_FOUND if audit should be skipped. */ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor, @@ -214,6 +224,11 @@ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor, // Caller will need to handle this — we return QUOTA_UNKNOWN to signal return AviatorSSCAuditHelper.QUOTA_UNKNOWN; } + // Fail closed when user requested quota protection but cannot determine default quota + if (isSkipIfExceedingQuota()) { + LOG.warn("Could not retrieve default quota; cannot honor --skip-if-exceeding-quota. Audit will be skipped."); + return AviatorSSCAuditHelper.QUOTA_APP_NOT_FOUND; + } logger.progress("Warning: Could not retrieve default quota, proceeding with audit."); return AviatorSSCAuditHelper.QUOTA_UNKNOWN; } @@ -226,7 +241,8 @@ private long handleAppNotFound(AviatorUserSessionDescriptor sessionDescriptor, /** * Evaluates the resolved quota against the auditable issue count and returns - * a result node if audit should be skipped, or null to proceed with the audit. + * a result node if audit should be skipped/reported, or null to proceed with the audit. + * Fails closed when quota cannot be reliably determined and user requested quota protection. */ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor av, String effectiveAppName, long auditableIssueCount, long availableQuota, @@ -237,6 +253,15 @@ private JsonNode evaluateQuota(UnirestInstance unirest, SSCAppVersionDescriptor AviatorSSCAuditHelper.setOperationMessage(result, "Could not retrieve quota for application '" + effectiveAppName + "'"); return result; } + // Fail closed when user requested quota protection but cannot determine quota + if (isSkipIfExceedingQuota()) { + LOG.warn("Could not retrieve quota; cannot honor --skip-if-exceeding-quota for {}:{}. Audit skipped.", + av.getApplicationName(), av.getVersionName()); + ObjectNode result = AviatorSSCAuditHelper.buildResultNode(av, null, "SKIPPED"); + AviatorSSCAuditHelper.setOperationMessage(result, + "Could not retrieve quota; audit skipped per --skip-if-exceeding-quota"); + return result; + } logger.progress("Warning: Could not retrieve quota for '%s', proceeding with audit.", effectiveAppName); } else if (availableQuota >= 0 && auditableIssueCount > availableQuota) { checkedQuotaBefore = availableQuota; diff --git a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/DastFprCorrelationEnricher.java b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/DastFprCorrelationEnricher.java index dd6a0d482ac..7594d29d4e1 100644 --- a/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/DastFprCorrelationEnricher.java +++ b/fcli-core/fcli-aviator/src/main/java/com/fortify/cli/aviator/ssc/helper/DastFprCorrelationEnricher.java @@ -159,6 +159,17 @@ private Map> groupByDastId(List pai @SneakyThrows private Document parseXml(Path path) { var factory = DocumentBuilderFactory.newInstance(); + // Disable DOCTYPE declarations and external entity processing to prevent XXE attacks + try { + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + } catch (Exception e) { + LOG.warn("Could not configure XXE protection for DocumentBuilderFactory; some protections may be unavailable: {}", + e.getMessage()); + } factory.setNamespaceAware(false); return factory.newDocumentBuilder().parse(Files.newInputStream(path)); }