Skip to content
Closed
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 @@ -13,6 +13,7 @@
import java.util.Scanner;
import java.util.regex.Pattern;

import io.swagger.codegen.utils.SecureFileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -21,11 +22,13 @@ public abstract class AbstractGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGenerator.class);

@SuppressWarnings("static-method")
public File writeToFile(String filename, String contents) throws IOException {
public File writeToFile(String filename, String contents) throws IOException, SecurityException {
LOGGER.info("writing file " + filename);
SecureFileUtils.validatePath(filename);
File output = new File(filename);

if (output.getParent() != null && !new File(output.getParent()).exists()) {
SecureFileUtils.validatePath(output.getParent());
File parent = new File(output.getParent());
parent.mkdirs();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.models.properties.UntypedProperty;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringEscapeUtils;
Expand Down Expand Up @@ -3562,6 +3563,11 @@ public String apiTestFilename(String templateName, String tag) {
}

public boolean shouldOverwrite(String filename) {
try {
SecureFileUtils.validatePath(filename);
} catch (SecurityException e) {
return false;
}
return !(skipOverwrite && new File(filename).exists());
}

Expand Down Expand Up @@ -3825,10 +3831,13 @@ public void writeOptional(String outputFolder, SupportingFile supportingFile) {
else {
folder = supportingFile.destinationFilename;
}
if(!new File(folder).exists()) {
try {
SecureFileUtils.validatePath(folder);
if (!new File(folder).exists()) {
LOGGER.info("Skipped overwriting " + supportingFile.destinationFilename + " as the file already exists in " + folder);
}
} catch (Exception e) {
supportingFiles.add(supportingFile);
} else {
LOGGER.info("Skipped overwriting " + supportingFile.destinationFilename + " as the file already exists in " + folder);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.codegen.ignore.CodegenIgnoreProcessor;
import io.swagger.codegen.languages.AbstractJavaCodegen;
import io.swagger.codegen.utils.ImplementationVersion;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.models.*;
import io.swagger.models.auth.OAuth2Definition;
import io.swagger.models.auth.SecuritySchemeDefinition;
Expand Down Expand Up @@ -597,6 +597,7 @@ protected void generateSupportingFiles(List<File> files, Map<String, Object> bun
if (StringUtils.isNotEmpty(support.folder)) {
outputFolder += File.separator + support.folder;
}
SecureFileUtils.validatePath(outputFolder);
File of = new File(outputFolder);
if (!of.isDirectory()) {
of.mkdirs();
Expand Down Expand Up @@ -671,6 +672,7 @@ public Reader getTemplate(String name) {
// Output .swagger-codegen-ignore if it doesn't exist and wasn't explicitly created by a generator
final String swaggerCodegenIgnore = ".swagger-codegen-ignore";
String ignoreFileNameTarget = config.outputFolder() + File.separator + swaggerCodegenIgnore;
SecureFileUtils.validatePath(ignoreFileNameTarget);
File ignoreFile = new File(ignoreFileNameTarget);
if (isGenerateSwaggerMetadata && !ignoreFile.exists()) {
String ignoreFileNameSource = File.separator + config.getCommonTemplateDir() + File.separator + swaggerCodegenIgnore;
Expand Down Expand Up @@ -785,6 +787,7 @@ public List<File> generate() {

protected File processTemplateToFile(Map<String, Object> templateData, String templateName, String outputFilename) throws IOException {
String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar);
SecureFileUtils.validatePath(adjustedOutputFilename);
if (ignoreProcessor.allowsFile(new File(adjustedOutputFilename))) {
String templateFile = getFullTemplateFile(config, templateName);
String template = readTemplate(templateFile);
Expand All @@ -801,6 +804,7 @@ public Reader getTemplate(String name) {
.compile(template);

writeToFile(adjustedOutputFilename, tmpl.execute(templateData));
SecureFileUtils.validatePath(adjustedOutputFilename);
return new File(adjustedOutputFilename);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.collect.ImmutableList;
import io.swagger.codegen.ignore.rules.DirectoryRule;
import io.swagger.codegen.ignore.rules.Rule;
import io.swagger.codegen.utils.SecureFileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -40,12 +41,19 @@ public CodegenIgnoreProcessor(final String baseDirectory) {
*/
@SuppressWarnings("WeakerAccess")
public CodegenIgnoreProcessor(final String baseDirectory, final String ignoreFile) {
final File directory = new File(baseDirectory);
final File targetIgnoreFile = new File(directory, ignoreFile);
if (directory.exists() && directory.isDirectory()) {
loadFromFile(targetIgnoreFile);
} else {
LOGGER.warn("Output directory does not exist, or is inaccessible. No file (.swagger-codegen-ignore) will be evaluated.");
try {
SecureFileUtils.validatePath(baseDirectory);
final File directory = new File(baseDirectory);
SecureFileUtils.validatePath(directory);
SecureFileUtils.validatePath(ignoreFile);
final File targetIgnoreFile = new File(directory, ignoreFile);
if (directory.exists() && directory.isDirectory()) {
loadFromFile(targetIgnoreFile);
} else {
LOGGER.warn("Output directory does not exist, or is inaccessible. No file (.swagger-codegen-ignore) will be evaluated.");
}
} catch (SecurityException e) {
LOGGER.error("Security violation: attempted to access unsafe ignore file path ", e);
}
}

Expand All @@ -55,7 +63,12 @@ public CodegenIgnoreProcessor(final String baseDirectory, final String ignoreFil
* @param targetIgnoreFile The ignore file location.
*/
public CodegenIgnoreProcessor(final File targetIgnoreFile) {
loadFromFile(targetIgnoreFile);
try {
SecureFileUtils.validatePath(targetIgnoreFile);
loadFromFile(targetIgnoreFile);
} catch (SecurityException e) {
LOGGER.error("Security violation: attempted to access unsafe ignore file path: " + targetIgnoreFile.getAbsolutePath(), e);
}
}

private void loadFromFile(File targetIgnoreFile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.codegen.CodegenParameter;
import io.swagger.codegen.CodegenProperty;
import io.swagger.codegen.SupportingFile;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;
import io.swagger.models.parameters.Parameter;
Expand Down Expand Up @@ -197,7 +198,11 @@ public void preprocessSwagger(Swagger swagger) {
//copy input swagger to output folder
try {
String swaggerJson = Json.pretty(swagger);
FileUtils.writeStringToFile(new File(outputFolder + File.separator + "swagger.json"), swaggerJson, StandardCharsets.UTF_8);
SecureFileUtils.validatePath(outputFolder);
File outputFile = new File(outputFolder + File.separator + "swagger.json");
FileUtils.writeStringToFile(outputFile, swaggerJson, StandardCharsets.UTF_8);
} catch (SecurityException e) {
throw new RuntimeException("Security violation: attempted to write to unsafe file path: " + outputFolder + File.separator + "swagger.json", e);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e.getCause());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.swagger.codegen.CodegenType;
import io.swagger.codegen.DefaultCodegen;
import io.swagger.codegen.SupportingFile;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.models.Model;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;
Expand Down Expand Up @@ -741,7 +742,13 @@ public void setGemAuthorEmail(String gemAuthorEmail) {
@Override
public boolean shouldOverwrite(String filename) {
// skip spec file as the file might have been updated with new test cases
return !(skipOverwrite && new File(filename).exists());
try {
SecureFileUtils.validatePath(filename);
return !(skipOverwrite && new File(filename).exists());
} catch (SecurityException e) {
LOGGER.warn("Security violation: attempted to check unsafe file path: " + filename, e);
return false;
}
//
//return super.shouldOverwrite(filename) && !filename.endsWith("_spec.rb");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import java.util.Map;

import io.swagger.codegen.CliOption;
import io.swagger.codegen.CodegenConstants;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.models.Model;
import io.swagger.models.properties.Property;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -63,6 +62,7 @@ public void processSwagger(Swagger swagger) {

try {
String outputFile = outputFolder + File.separator + this.outputFile;
SecureFileUtils.validatePath(outputFile);
FileUtils.writeStringToFile(new File(outputFile), swaggerString, StandardCharsets.UTF_8);
LOGGER.debug("wrote file to " + outputFile);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import io.swagger.codegen.*;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.jackson.mixin.OperationResponseMixin;
import io.swagger.jackson.mixin.ResponseSchemaMixin;
import io.swagger.models.Model;
Expand Down Expand Up @@ -104,6 +105,7 @@ public void processSwagger(Swagger swagger) {
configureMapper(mapper);
String swaggerString = mapper.writeValueAsString(swagger);
String outputFile = outputFolder + File.separator + this.outputFile;
SecureFileUtils.validatePath(outputFile);
FileUtils.writeStringToFile(new File(outputFile), swaggerString, StandardCharsets.UTF_8);
LOGGER.debug("wrote file to " + outputFile);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.swagger.codegen.utils;

import java.io.File;
import java.io.IOException;


/**
* Utility class for secure file operations that prevent path traversal attacks.
* Uses a simplified approach focusing on canonical path validation and allowlist-based security.
*/
public class SecureFileUtils {

public static void validatePath(File file) throws SecurityException {
if (file == null) {
throw new IllegalArgumentException("File cannot be null");
}

try {
String absolutePath = file.getAbsolutePath();
String canonicalPath = file.getCanonicalPath();

if (absolutePath.contains("..") || absolutePath.contains("\0")) {
throw new SecurityException("Path contains suspicious characters: " + absolutePath);
}

if (canonicalPath.contains("..") || canonicalPath.contains("\0")) {
throw new SecurityException("Path contains suspicious characters: " + canonicalPath);
}

} catch (IOException e) {
throw new SecurityException("Unable to resolve canonical path for: " + file.getAbsolutePath(), e);
}
}

public static void validatePath(String path) throws SecurityException {
if (path == null || path.trim().isEmpty()) {
throw new IllegalArgumentException("Path cannot be null or empty");
}

if (path.contains("..") || path.contains("\0")) {
throw new SecurityException("Path contains suspicious characters: " + path);
}

validatePath(new File(path));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.swagger.codegen;

import org.testng.annotations.Test;

import java.io.IOException;

public class AbstractGeneratorTest {


private static class TestableAbstractGenerator extends AbstractGenerator {
}

@Test(expectedExceptions = SecurityException.class)
public void testWriteToFileWithPathTraversal() throws IOException {
TestableAbstractGenerator generator = new TestableAbstractGenerator();
generator.writeToFile("../../../etc/passwd", "malicious content");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception {
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP), Boolean.FALSE);
Assert.assertEquals(codegen.isHideGenerationTimestamp(), false);
}

@Test
public void testShouldOverwriteWithPathTraversal() {
DefaultCodegen codegen = new DefaultCodegen();
boolean result = codegen.shouldOverwrite("../../../etc/passwd");

Assert.assertFalse(result, "shouldOverwrite should return false when SecurityException is thrown");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.rules.TemporaryFolder;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -45,6 +44,8 @@ public class DefaultGeneratorTest {

public TemporaryFolder folder = new TemporaryFolder();

private final DefaultGenerator generator = new DefaultGenerator();;

@BeforeMethod
public void setUp() throws Exception {
folder.create();
Expand Down Expand Up @@ -1134,4 +1135,9 @@ private static CodegenOperation findCodegenOperationByOperationId(Map<String, Li
}
return null;
}

@Test(expectedExceptions = SecurityException.class)
public void testProcessTemplateToFileWithPathTraversal() throws IOException {
generator.processTemplateToFile(new HashMap<>(), "template.mustache", "../../../etc/passwd");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.swagger.codegen.ignore;

import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;

public class CodegenIgnoreProcessorSecurityTest {

@Test
public void testConstructorWithPathTraversal() {
CodegenIgnoreProcessor processor = new CodegenIgnoreProcessor("../../../etc");

Assert.assertNotNull(processor, "Processor should be created despite security violation");
Assert.assertTrue(processor.getExclusionRules().isEmpty(), "No exclusion rules should be loaded");
Assert.assertTrue(processor.getInclusionRules().isEmpty(), "No inclusion rules should be loaded");
}

@Test
public void testFileConstructorWithPathTraversal() {
File maliciousFile = new File("../../../etc/passwd");
CodegenIgnoreProcessor processor = new CodegenIgnoreProcessor(maliciousFile);

Assert.assertNotNull(processor, "Processor should be created despite security violation");
Assert.assertTrue(processor.getExclusionRules().isEmpty(), "No exclusion rules should be loaded");
Assert.assertTrue(processor.getInclusionRules().isEmpty(), "No inclusion rules should be loaded");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.swagger.codegen.languages;

import org.testng.Assert;
import org.testng.annotations.Test;

public class RubyClientCodegenTest {

@Test
public void testShouldOverwriteWithPathTraversal() {
RubyClientCodegen codegen = new RubyClientCodegen();

boolean result = codegen.shouldOverwrite("../../../etc/passwd");

Assert.assertFalse(result, "shouldOverwrite should return false when SecurityException is thrown");
}
}
Loading
Loading