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 @@ -59,7 +59,21 @@ public String getFcliCmdArgs(Map<String, Object> 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<String> streamValueElements(Object value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,6 +69,12 @@ public List<String> readFileWithFallback(Path filePath) {
private List<String> 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"));
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private List<String> 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,
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ private Map<String, List<CorrelatedPair>> groupByDastId(List<CorrelatedPair> 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));
}
Expand Down
Loading