From 3d5a3f1b954b515a485d9192ac798513b25273ae Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Fri, 16 Jan 2026 11:18:50 +0530 Subject: [PATCH 1/8] Resolved the file upload bugs Signed-off-by: KashiwalHarsh --- .../signup/services/RegistrationService.java | 30 +++++++++++++++++-- .../io/mosip/signup/util/ErrorConstants.java | 2 ++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index 30eee544..d4660d75 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -330,9 +330,35 @@ public RegisterResponse uploadFile(String transactionId, String fieldName, Multi throw new SignUpException(ErrorConstants.INVALID_REQUEST); } + String contentType = file.getContentType(); + if(contentType == null || !contentType.startsWith("image/")) { + log.error("Invalid file type: {}. Only image formats are allowed.", contentType); + throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); + } + try { - RegistrationFiles registrationFiles = new RegistrationFiles(); - registrationFiles.getUploadedFiles().put(fieldName, Base64.getEncoder().encodeToString(file.getBytes())); + + String newFileBase64 = Base64.getEncoder().encodeToString(file.getBytes()); + String newFileHash = IdentityProviderUtil.generateB64EncodedHash(IdentityProviderUtil.ALGO_SHA3_256, newFileBase64); + + RegistrationFiles existingFiles = cacheUtilService.getRegistrationFiles(transactionId); + if (existingFiles != null && existingFiles.getUploadedFiles() != null) { + String existingFileBase64 = existingFiles.getUploadedFiles().get(fieldName); + if (existingFileBase64 != null) { + String existingFileHash = IdentityProviderUtil.generateB64EncodedHash(IdentityProviderUtil.ALGO_SHA3_256, existingFileBase64); + if (newFileHash.equals(existingFileHash)) { + log.error("Duplicate file upload detected for field: {}", fieldName); + throw new SignUpException(ErrorConstants.FILE_ALREADY_EXISTS); + } + log.info("Replacing existing file for field: {}", fieldName); + } + } + + RegistrationFiles registrationFiles = existingFiles != null ? existingFiles : new RegistrationFiles(); + if (registrationFiles.getUploadedFiles() == null) { + registrationFiles.setUploadedFiles(new HashMap<>()); + } + registrationFiles.getUploadedFiles().put(fieldName, newFileBase64); cacheUtilService.setRegistrationFiles(transactionId, registrationFiles); RegisterResponse registerResponse = new RegisterResponse(); diff --git a/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java b/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java index 82fca251..1692c506 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java +++ b/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java @@ -62,4 +62,6 @@ public class ErrorConstants { public static final String TOKEN_REQUEST_FAILED = "token_request_failed"; public static final String UPLOAD_FAILED = "upload_failed"; + public static final String INVALID_FILE_TYPE = "invalid_file_type"; + public static final String FILE_ALREADY_EXISTS = "file_already_exists"; } From cd4f0c3136fdc2f530489b9abb3acdbbe564e736 Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Fri, 23 Jan 2026 17:05:35 +0530 Subject: [PATCH 2/8] Fixed the file upload bug Signed-off-by: KashiwalHarsh --- signup-service/pom.xml | 5 ++ .../signup/services/RegistrationService.java | 50 +++++++++---------- .../io/mosip/signup/util/UiSpecUtils.java | 32 ++++++++++++ .../services/RegistrationServiceTest.java | 25 ++++++++-- 4 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java diff --git a/signup-service/pom.xml b/signup-service/pom.xml index 140dc466..a73a816a 100644 --- a/signup-service/pom.xml +++ b/signup-service/pom.xml @@ -80,6 +80,11 @@ httpclient5 5.3.1 + + org.apache.tika + tika-core + 2.9.2 + org.bouncycastle bcprov-jdk18on diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index d4660d75..f0aba47a 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -30,13 +30,18 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Base64; +import org.apache.tika.Tika; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; import static io.mosip.signup.util.SignUpConstants.*; @@ -98,6 +103,8 @@ public class RegistrationService { @Value("${mosip.signup.file.fieldname.regex:[A-Za-z0-9_-]+}") private String fileFieldNameRegex; + private final Tika tika = new Tika(); + /** * Generate and regenerate challenge based on the "regenerate" flag in the request. * if regenerate is false - always creates a new transaction and set-cookie header is sent in the response. @@ -330,35 +337,28 @@ public RegisterResponse uploadFile(String transactionId, String fieldName, Multi throw new SignUpException(ErrorConstants.INVALID_REQUEST); } - String contentType = file.getContentType(); - if(contentType == null || !contentType.startsWith("image/")) { - log.error("Invalid file type: {}. Only image formats are allowed.", contentType); - throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); + byte[] fileBytes; + try { + fileBytes = file.getBytes(); + } catch (IOException e) { + log.error("Failed to read uploaded file bytes", e); + throw new SignUpException(ErrorConstants.UPLOAD_FAILED); } - try { + JsonNode uiSpec = profileRegistryPlugin.getUISpecification(); + Set allowedTypes = UiSpecUtils.findAcceptedFileTypes(uiSpec); - String newFileBase64 = Base64.getEncoder().encodeToString(file.getBytes()); - String newFileHash = IdentityProviderUtil.generateB64EncodedHash(IdentityProviderUtil.ALGO_SHA3_256, newFileBase64); - - RegistrationFiles existingFiles = cacheUtilService.getRegistrationFiles(transactionId); - if (existingFiles != null && existingFiles.getUploadedFiles() != null) { - String existingFileBase64 = existingFiles.getUploadedFiles().get(fieldName); - if (existingFileBase64 != null) { - String existingFileHash = IdentityProviderUtil.generateB64EncodedHash(IdentityProviderUtil.ALGO_SHA3_256, existingFileBase64); - if (newFileHash.equals(existingFileHash)) { - log.error("Duplicate file upload detected for field: {}", fieldName); - throw new SignUpException(ErrorConstants.FILE_ALREADY_EXISTS); - } - log.info("Replacing existing file for field: {}", fieldName); - } - } + String detectedMimeType = tika.detect(fileBytes); - RegistrationFiles registrationFiles = existingFiles != null ? existingFiles : new RegistrationFiles(); - if (registrationFiles.getUploadedFiles() == null) { - registrationFiles.setUploadedFiles(new HashMap<>()); - } - registrationFiles.getUploadedFiles().put(fieldName, newFileBase64); + if (!allowedTypes.contains(detectedMimeType)) { + log.error("Invalid file type detected. Declared: {}, Detected: {}. Allowed: {}", + file.getContentType(), detectedMimeType, allowedTypes); + throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); + } + + try { + RegistrationFiles registrationFiles = new RegistrationFiles(); + registrationFiles.getUploadedFiles().put(fieldName, Base64.getEncoder().encodeToString(file.getBytes())); cacheUtilService.setRegistrationFiles(transactionId, registrationFiles); RegisterResponse registerResponse = new RegisterResponse(); diff --git a/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java new file mode 100644 index 00000000..b414a2cb --- /dev/null +++ b/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java @@ -0,0 +1,32 @@ +package io.mosip.signup.util; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.*; + +public class UiSpecUtils { + public static Set findAcceptedFileTypes(JsonNode root) { + Set result = new HashSet<>(); + if (root == null) return result; + + JsonNode schemaNode = root.get("schema"); + if (schemaNode == null || !schemaNode.isArray()) return result; + + for (JsonNode field : schemaNode) { + JsonNode acceptedFileTypes = field.get("acceptedFileTypes"); + if (acceptedFileTypes == null) continue; + + if (acceptedFileTypes.isArray()) { + for (JsonNode typeNode : acceptedFileTypes) { + if (typeNode.isTextual()) { + result.add(typeNode.asText().trim()); + } + } + } else if (acceptedFileTypes.isTextual()) { + Arrays.stream(acceptedFileTypes.asText().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(result::add); + } + } + return result; + } +} \ No newline at end of file diff --git a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java index b1556751..c8b6c6ae 100644 --- a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java +++ b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java @@ -5,6 +5,7 @@ */ package io.mosip.signup.services; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.mosip.esignet.core.exception.EsignetException; @@ -1885,20 +1886,38 @@ public void getUiSpec_withException_theFail() { } @Test - public void uploadFile_withValidTransaction_thenPass() { + public void uploadFile_withValidTransaction_thenPass() throws JsonProcessingException { String transactionId = "txn-123"; String fieldName = "photo"; - byte[] fileContent = "test-image".getBytes(); - MultipartFile file = new MockMultipartFile("file", fileContent); + byte[] pngBytes = new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52 + }; + MultipartFile file = new MockMultipartFile("file", "photo.png", "image/png", pngBytes); RegistrationTransaction transaction = new RegistrationTransaction("user", Purpose.REGISTRATION); when(cacheUtilService.getChallengeVerifiedTransaction(transactionId)).thenReturn(transaction); + String uiSpecJson = """ + { + "schema": [ + { + "id": "photo", + "controlType": "fileupload", + "acceptedFileTypes": ["image/jpeg", "image/png"] + } + ] + } + """; + JsonNode uiSpecNode = objectMapper.readTree(uiSpecJson); + when(profileRegistryPlugin.getUISpecification()).thenReturn(uiSpecNode); + RegisterResponse response = registrationService.uploadFile(transactionId, fieldName, file); Assert.assertNotNull(response); Assert.assertEquals(ActionStatus.UPLOADED, response.getStatus()); verify(cacheUtilService, times(1)).setRegistrationFiles(eq(transactionId), any(RegistrationFiles.class)); + verify(profileRegistryPlugin, times(1)).getUISpecification(); } @Test(expected = InvalidTransactionException.class) From 1a813a66cb065204ddb8bafe81d42951d63512ba Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Tue, 27 Jan 2026 14:43:01 +0530 Subject: [PATCH 3/8] added validation for filedName as per controlType Signed-off-by: KashiwalHarsh --- signup-service/pom.xml | 5 - .../signup/services/RegistrationService.java | 54 +++++---- .../io/mosip/signup/util/UiSpecUtils.java | 32 ----- .../io/mosip/signup/util/UploadFileUtils.java | 110 ++++++++++++++++++ 4 files changed, 142 insertions(+), 59 deletions(-) delete mode 100644 signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java create mode 100644 signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java diff --git a/signup-service/pom.xml b/signup-service/pom.xml index a73a816a..140dc466 100644 --- a/signup-service/pom.xml +++ b/signup-service/pom.xml @@ -80,11 +80,6 @@ httpclient5 5.3.1 - - org.apache.tika - tika-core - 2.9.2 - org.bouncycastle bcprov-jdk18on diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index f0aba47a..2d517158 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -30,18 +30,13 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; - -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.Base64; -import org.apache.tika.Tika; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; -import java.util.stream.Collectors; import static io.mosip.signup.util.SignUpConstants.*; @@ -103,8 +98,6 @@ public class RegistrationService { @Value("${mosip.signup.file.fieldname.regex:[A-Za-z0-9_-]+}") private String fileFieldNameRegex; - private final Tika tika = new Tika(); - /** * Generate and regenerate challenge based on the "regenerate" flag in the request. * if regenerate is false - always creates a new transaction and set-cookie header is sent in the response. @@ -324,19 +317,7 @@ public JsonNode getUiSpec() { return profileRegistryPlugin.getUISpecification(); } - public RegisterResponse uploadFile(String transactionId, String fieldName, MultipartFile file) throws SignUpException { - RegistrationTransaction transaction = cacheUtilService.getChallengeVerifiedTransaction(transactionId); - if(transaction == null) { - log.error("Transaction {} : not found in ChallengeVerifiedTransaction cache", transactionId); - throw new InvalidTransactionException(); - } - - if(fieldName == null || fieldName.isBlank() || !fieldName.matches(fileFieldNameRegex) - || file == null || file.isEmpty()) { - log.error("Invalid fieldName or file {} {}", fieldName, file); - throw new SignUpException(ErrorConstants.INVALID_REQUEST); - } - + private void validateFieldAndFile(MultipartFile file, String fieldName) { byte[] fileBytes; try { fileBytes = file.getBytes(); @@ -346,15 +327,44 @@ public RegisterResponse uploadFile(String transactionId, String fieldName, Multi } JsonNode uiSpec = profileRegistryPlugin.getUISpecification(); - Set allowedTypes = UiSpecUtils.findAcceptedFileTypes(uiSpec); + UploadFileUtils.FileTypeConfig config = UploadFileUtils.extractFileUploadConfig(uiSpec); - String detectedMimeType = tika.detect(fileBytes); + Set allowedTypes = config.getAcceptedFileTypes(); + Set allowedFieldNames = config.getFieldNames(); + + String detectedMimeType = UploadFileUtils.detectMimeType(fileBytes); + + if ("application/octet-stream".equals(detectedMimeType)) { + log.error("Unrecognized file type for field: {}", fieldName); + throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); + } + + if (!allowedFieldNames.contains(fieldName)) { + log.error("Invalid fieldName for file {} {}", fieldName, file); + throw new SignUpException(ErrorConstants.INVALID_REQUEST); + } if (!allowedTypes.contains(detectedMimeType)) { log.error("Invalid file type detected. Declared: {}, Detected: {}. Allowed: {}", file.getContentType(), detectedMimeType, allowedTypes); throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); } + } + + public RegisterResponse uploadFile(String transactionId, String fieldName, MultipartFile file) throws SignUpException { + RegistrationTransaction transaction = cacheUtilService.getChallengeVerifiedTransaction(transactionId); + if(transaction == null) { + log.error("Transaction {} : not found in ChallengeVerifiedTransaction cache", transactionId); + throw new InvalidTransactionException(); + } + + if(fieldName == null || fieldName.isBlank() || !fieldName.matches(fileFieldNameRegex) + || file == null || file.isEmpty()) { + log.error("Invalid fieldName or file {} {}", fieldName, file); + throw new SignUpException(ErrorConstants.INVALID_REQUEST); + } + + validateFieldAndFile(file, fieldName); try { RegistrationFiles registrationFiles = new RegistrationFiles(); diff --git a/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java deleted file mode 100644 index b414a2cb..00000000 --- a/signup-service/src/main/java/io/mosip/signup/util/UiSpecUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.mosip.signup.util; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.*; - -public class UiSpecUtils { - public static Set findAcceptedFileTypes(JsonNode root) { - Set result = new HashSet<>(); - if (root == null) return result; - - JsonNode schemaNode = root.get("schema"); - if (schemaNode == null || !schemaNode.isArray()) return result; - - for (JsonNode field : schemaNode) { - JsonNode acceptedFileTypes = field.get("acceptedFileTypes"); - if (acceptedFileTypes == null) continue; - - if (acceptedFileTypes.isArray()) { - for (JsonNode typeNode : acceptedFileTypes) { - if (typeNode.isTextual()) { - result.add(typeNode.asText().trim()); - } - } - } else if (acceptedFileTypes.isTextual()) { - Arrays.stream(acceptedFileTypes.asText().split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .forEach(result::add); - } - } - return result; - } -} \ No newline at end of file diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java new file mode 100644 index 00000000..cdf39288 --- /dev/null +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -0,0 +1,110 @@ +package io.mosip.signup.util; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.*; + +public class UploadFileUtils { + + private static final Map MAGIC_SIGNATURES = Map.of( + "89504E47", "image/png", + "FFD8FF", "image/jpeg", + "52494646", "image/webp", + "25504446", "application/pdf", + "504B0304", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "D0CF11E0", "application/msword" + ); + + public static String detectMimeType(byte[] fileBytes) { + if (fileBytes == null || fileBytes.length < 4) { + return "application/octet-stream"; + } + + StringBuilder hexBuilder = new StringBuilder(); + int bytesToCheck = Math.min(fileBytes.length, 12); + + for (int i = 0; i < bytesToCheck; i++) { + hexBuilder.append(String.format("%02X", fileBytes[i])); + } + + String hexString = hexBuilder.toString(); + + // Check for WebP specifically (RIFF....WEBP pattern) + if (hexString.startsWith("52494646") && hexString.length() >= 24 + && hexString.substring(16, 24).equals("57454250")) { + return "image/webp"; + } + + // Check other signatures + for (Map.Entry entry : MAGIC_SIGNATURES.entrySet()) { + if (hexString.startsWith(entry.getKey()) && !entry.getKey().equals("52494646")) { + return entry.getValue(); + } + } + + return "application/octet-stream"; + } + + public static class FileTypeConfig { + private final Set acceptedFileTypes; + private final Set fieldNames; + + public FileTypeConfig(Set acceptedFileTypes, Set fieldNames) { + this.acceptedFileTypes = acceptedFileTypes; + this.fieldNames = fieldNames; + } + + public Set getAcceptedFileTypes() { + return acceptedFileTypes; + } + + public Set getFieldNames() { + return fieldNames; + } + } + public static FileTypeConfig extractFileUploadConfig(JsonNode root) { + Set acceptedFileTypes = new HashSet<>(); + Set fieldNames = new HashSet<>(); + + if (root == null) return new FileTypeConfig(acceptedFileTypes, fieldNames); + + JsonNode schemaNode = root.get("schema"); + if (schemaNode == null || !schemaNode.isArray()) return new FileTypeConfig(acceptedFileTypes, fieldNames); + + for (JsonNode field : schemaNode) { + JsonNode controlTypeNode = field.get("controlType"); + if (controlTypeNode == null || !controlTypeNode.isTextual()) continue; + + String controlType = controlTypeNode.asText().trim(); + if (!controlType.equalsIgnoreCase("photo") && !controlType.equalsIgnoreCase("fileUpload")) { + continue; + } + + // Extract field ID + JsonNode idNode = field.get("id"); + if (idNode != null && idNode.isTextual()) { + fieldNames.add(idNode.asText().trim()); + } + + // Extract accepted file types + JsonNode acceptedFileTypesNode = field.get("acceptedFileTypes"); + if (acceptedFileTypesNode == null) continue; + + if (acceptedFileTypesNode.isArray()) { + for (JsonNode typeNode : acceptedFileTypesNode) { + if (typeNode.isTextual()) { + String trimmed = typeNode.asText().trim(); + if (!trimmed.isEmpty()) { + acceptedFileTypes.add(trimmed); + } + } + } + } else if (acceptedFileTypesNode.isTextual()) { + Arrays.stream(acceptedFileTypesNode.asText().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(acceptedFileTypes::add); + } + } + + return new FileTypeConfig(acceptedFileTypes, fieldNames); + } +} \ No newline at end of file From 1009f46354757a37c2f18e7821e9e7300d4d50d9 Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Tue, 27 Jan 2026 16:19:26 +0530 Subject: [PATCH 4/8] accepts file based on fieldName property Signed-off-by: KashiwalHarsh --- .../signup/services/RegistrationService.java | 14 +-- .../io/mosip/signup/util/UploadFileUtils.java | 65 ++++++----- .../services/RegistrationServiceTest.java | 104 ++++++++++++++++++ 3 files changed, 139 insertions(+), 44 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index 2d517158..4ef334d1 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -329,9 +329,6 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { JsonNode uiSpec = profileRegistryPlugin.getUISpecification(); UploadFileUtils.FileTypeConfig config = UploadFileUtils.extractFileUploadConfig(uiSpec); - Set allowedTypes = config.getAcceptedFileTypes(); - Set allowedFieldNames = config.getFieldNames(); - String detectedMimeType = UploadFileUtils.detectMimeType(fileBytes); if ("application/octet-stream".equals(detectedMimeType)) { @@ -339,14 +336,11 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); } - if (!allowedFieldNames.contains(fieldName)) { - log.error("Invalid fieldName for file {} {}", fieldName, file); - throw new SignUpException(ErrorConstants.INVALID_REQUEST); - } + Set allowedTypesForField = config.getAcceptedTypesForField(fieldName); - if (!allowedTypes.contains(detectedMimeType)) { - log.error("Invalid file type detected. Declared: {}, Detected: {}. Allowed: {}", - file.getContentType(), detectedMimeType, allowedTypes); + if (allowedTypesForField.isEmpty() || !allowedTypesForField.contains(detectedMimeType)) { + log.error("Invalid file type for field: {}. Detected: {}, Allowed: {}", + fieldName, detectedMimeType, allowedTypesForField); throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); } } diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java index cdf39288..3d07b8ca 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -1,6 +1,7 @@ package io.mosip.signup.util; import com.fasterxml.jackson.databind.JsonNode; import java.util.*; +import java.util.stream.Collectors; public class UploadFileUtils { @@ -22,7 +23,7 @@ public static String detectMimeType(byte[] fileBytes) { int bytesToCheck = Math.min(fileBytes.length, 12); for (int i = 0; i < bytesToCheck; i++) { - hexBuilder.append(String.format("%02X", fileBytes[i])); + hexBuilder.append(String.format("%02X", fileBytes[i] & 0xFF)); } String hexString = hexBuilder.toString(); @@ -44,30 +45,22 @@ public static String detectMimeType(byte[] fileBytes) { } public static class FileTypeConfig { - private final Set acceptedFileTypes; - private final Set fieldNames; + private final Map> fieldAcceptedTypes; - public FileTypeConfig(Set acceptedFileTypes, Set fieldNames) { - this.acceptedFileTypes = acceptedFileTypes; - this.fieldNames = fieldNames; + public FileTypeConfig(Map> fieldAcceptedTypes) { + this.fieldAcceptedTypes = fieldAcceptedTypes; } - - public Set getAcceptedFileTypes() { - return acceptedFileTypes; - } - - public Set getFieldNames() { - return fieldNames; + public Set getAcceptedTypesForField(String fieldName) { + return fieldAcceptedTypes.getOrDefault(fieldName, Collections.emptySet()); } } public static FileTypeConfig extractFileUploadConfig(JsonNode root) { - Set acceptedFileTypes = new HashSet<>(); - Set fieldNames = new HashSet<>(); + Map> fieldAcceptedTypes = new LinkedHashMap<>(); - if (root == null) return new FileTypeConfig(acceptedFileTypes, fieldNames); + if (root == null) return new FileTypeConfig(fieldAcceptedTypes); JsonNode schemaNode = root.get("schema"); - if (schemaNode == null || !schemaNode.isArray()) return new FileTypeConfig(acceptedFileTypes, fieldNames); + if (schemaNode == null || !schemaNode.isArray()) return new FileTypeConfig(fieldAcceptedTypes); for (JsonNode field : schemaNode) { JsonNode controlTypeNode = field.get("controlType"); @@ -80,31 +73,35 @@ public static FileTypeConfig extractFileUploadConfig(JsonNode root) { // Extract field ID JsonNode idNode = field.get("id"); - if (idNode != null && idNode.isTextual()) { - fieldNames.add(idNode.asText().trim()); - } + if (idNode == null || !idNode.isTextual()) continue; + String fieldName = idNode.asText().trim(); + if (fieldName.isEmpty()) continue; + + Set acceptedTypes = new HashSet<>(); // Extract accepted file types JsonNode acceptedFileTypesNode = field.get("acceptedFileTypes"); - if (acceptedFileTypesNode == null) continue; - - if (acceptedFileTypesNode.isArray()) { - for (JsonNode typeNode : acceptedFileTypesNode) { - if (typeNode.isTextual()) { - String trimmed = typeNode.asText().trim(); - if (!trimmed.isEmpty()) { - acceptedFileTypes.add(trimmed); + if (acceptedFileTypesNode != null) { + if (acceptedFileTypesNode.isArray()) { + for (JsonNode typeNode : acceptedFileTypesNode) { + if (typeNode.isTextual()) { + String trimmed = typeNode.asText().trim(); + if (!trimmed.isEmpty()) { + acceptedTypes.add(trimmed); + } } } + } else if (acceptedFileTypesNode.isTextual()) { + Arrays.stream(acceptedFileTypesNode.asText().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(acceptedTypes::add); } - } else if (acceptedFileTypesNode.isTextual()) { - Arrays.stream(acceptedFileTypesNode.asText().split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .forEach(acceptedFileTypes::add); } + + fieldAcceptedTypes.put(fieldName, acceptedTypes); } - return new FileTypeConfig(acceptedFileTypes, fieldNames); + return new FileTypeConfig(fieldAcceptedTypes); } } \ No newline at end of file diff --git a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java index c8b6c6ae..d5a4158f 100644 --- a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java +++ b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java @@ -1920,6 +1920,110 @@ public void uploadFile_withValidTransaction_thenPass() throws JsonProcessingExce verify(profileRegistryPlugin, times(1)).getUISpecification(); } + @Test + public void uploadFile_withUnrecognizedFileType_throwsInvalidFileType() throws JsonProcessingException { + String transactionId = "txn-123"; + String fieldName = "photo"; + // Random bytes that don't match any known file signature + byte[] unknownBytes = new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + MultipartFile file = new MockMultipartFile("file", "unknown.bin", "application/octet-stream", unknownBytes); + + RegistrationTransaction transaction = new RegistrationTransaction("user", Purpose.REGISTRATION); + when(cacheUtilService.getChallengeVerifiedTransaction(transactionId)).thenReturn(transaction); + + // UI spec with "photo" field that accepts image types + String uiSpecJson = """ + { + "schema": [ + { + "id": "photo", + "controlType": "photo", + "acceptedFileTypes": ["image/png", "image/jpeg"] + } + ] + } + """; + JsonNode uiSpecNode = objectMapper.readTree(uiSpecJson); + when(profileRegistryPlugin.getUISpecification()).thenReturn(uiSpecNode); + + try { + registrationService.uploadFile(transactionId, fieldName, file); + Assert.fail("Expected SignUpException to be thrown"); + } catch (SignUpException e) { + Assert.assertEquals(ErrorConstants.INVALID_FILE_TYPE, e.getErrorCode()); + } + } + + @Test + public void uploadFile_withFieldNameNotInUISpec_throwsInvalidFileType() throws JsonProcessingException { + String transactionId = "txn-123"; + String fieldName = "unknownField"; + // Valid PNG magic bytes + byte[] pngBytes = new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A + }; + MultipartFile file = new MockMultipartFile("file", "photo.png", "image/png", pngBytes); + + RegistrationTransaction transaction = new RegistrationTransaction("user", Purpose.REGISTRATION); + when(cacheUtilService.getChallengeVerifiedTransaction(transactionId)).thenReturn(transaction); + + // UI spec with only "photo" field - "unknownField" is not present, so allowedTypesForField will be empty + String uiSpecJson = """ + { + "schema": [ + { + "id": "photo", + "controlType": "fileUpload", + "acceptedFileTypes": ["image/png", "image/jpeg"] + } + ] + } + """; + JsonNode uiSpecNode = objectMapper.readTree(uiSpecJson); + when(profileRegistryPlugin.getUISpecification()).thenReturn(uiSpecNode); + + try { + registrationService.uploadFile(transactionId, fieldName, file); + Assert.fail("Expected SignUpException to be thrown"); + } catch (SignUpException e) { + Assert.assertEquals(ErrorConstants.INVALID_FILE_TYPE, e.getErrorCode()); + } + } + + @Test + public void uploadFile_withMimeTypeNotInAllowedTypes_throwsInvalidFileType() throws JsonProcessingException { + String transactionId = "txn-123"; + String fieldName = "photo"; + // Valid PDF magic bytes (25504446 = %PDF) + byte[] pdfBytes = new byte[]{0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34}; + MultipartFile file = new MockMultipartFile("file", "document.pdf", "application/pdf", pdfBytes); + + RegistrationTransaction transaction = new RegistrationTransaction("user", Purpose.REGISTRATION); + when(cacheUtilService.getChallengeVerifiedTransaction(transactionId)).thenReturn(transaction); + + // UI spec with "photo" field that only accepts image/png and image/jpeg, NOT application/pdf + String uiSpecJson = """ + { + "schema": [ + { + "id": "photo", + "controlType": "fileUpload", + "acceptedFileTypes": ["image/png", "image/jpeg"] + } + ] + } + """; + JsonNode uiSpecNode = objectMapper.readTree(uiSpecJson); + when(profileRegistryPlugin.getUISpecification()).thenReturn(uiSpecNode); + + try { + registrationService.uploadFile(transactionId, fieldName, file); + Assert.fail("Expected SignUpException to be thrown"); + } catch (SignUpException e) { + Assert.assertEquals(ErrorConstants.INVALID_FILE_TYPE, e.getErrorCode()); + } + } + @Test(expected = InvalidTransactionException.class) public void uploadFile_withInvalidTransaction_thenFail() { String transactionId = "invalid-txn"; From 699d464e063bfab524c9be01911588295d5dec68 Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Wed, 28 Jan 2026 00:56:41 +0530 Subject: [PATCH 5/8] code cleanup and exception handling Signed-off-by: KashiwalHarsh --- .../signup/services/RegistrationService.java | 11 +-- .../io/mosip/signup/util/ErrorConstants.java | 2 +- .../io/mosip/signup/util/UploadFileUtils.java | 71 +++++++++---------- .../services/RegistrationServiceTest.java | 16 ++--- 4 files changed, 46 insertions(+), 54 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index 4ef334d1..0cddf600 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -327,7 +327,12 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { } JsonNode uiSpec = profileRegistryPlugin.getUISpecification(); - UploadFileUtils.FileTypeConfig config = UploadFileUtils.extractFileUploadConfig(uiSpec); + Set allowedTypesForField = UploadFileUtils.getAcceptedTypesForField(uiSpec, fieldName); + + if (allowedTypesForField.isEmpty()) { + log.error("Invalid field for file upload: {}",fieldName); + throw new SignUpException(ErrorConstants.INVALID_FIELD); + } String detectedMimeType = UploadFileUtils.detectMimeType(fileBytes); @@ -336,9 +341,7 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); } - Set allowedTypesForField = config.getAcceptedTypesForField(fieldName); - - if (allowedTypesForField.isEmpty() || !allowedTypesForField.contains(detectedMimeType)) { + if (!allowedTypesForField.contains(detectedMimeType)) { log.error("Invalid file type for field: {}. Detected: {}, Allowed: {}", fieldName, detectedMimeType, allowedTypesForField); throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); diff --git a/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java b/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java index 1692c506..d7e4a427 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java +++ b/signup-service/src/main/java/io/mosip/signup/util/ErrorConstants.java @@ -63,5 +63,5 @@ public class ErrorConstants { public static final String TOKEN_REQUEST_FAILED = "token_request_failed"; public static final String UPLOAD_FAILED = "upload_failed"; public static final String INVALID_FILE_TYPE = "invalid_file_type"; - public static final String FILE_ALREADY_EXISTS = "file_already_exists"; + public static final String INVALID_FIELD = "invalid_field"; } diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java index 3d07b8ca..e26dad2f 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -44,23 +44,15 @@ public static String detectMimeType(byte[] fileBytes) { return "application/octet-stream"; } - public static class FileTypeConfig { - private final Map> fieldAcceptedTypes; - - public FileTypeConfig(Map> fieldAcceptedTypes) { - this.fieldAcceptedTypes = fieldAcceptedTypes; - } - public Set getAcceptedTypesForField(String fieldName) { - return fieldAcceptedTypes.getOrDefault(fieldName, Collections.emptySet()); + public static Set getAcceptedTypesForField(JsonNode root, String targetFieldName) { + if (root == null || targetFieldName == null || targetFieldName.isBlank()) { + return Collections.emptySet(); } - } - public static FileTypeConfig extractFileUploadConfig(JsonNode root) { - Map> fieldAcceptedTypes = new LinkedHashMap<>(); - - if (root == null) return new FileTypeConfig(fieldAcceptedTypes); JsonNode schemaNode = root.get("schema"); - if (schemaNode == null || !schemaNode.isArray()) return new FileTypeConfig(fieldAcceptedTypes); + if (schemaNode == null || !schemaNode.isArray()) { + return Collections.emptySet(); + } for (JsonNode field : schemaNode) { JsonNode controlTypeNode = field.get("controlType"); @@ -71,37 +63,38 @@ public static FileTypeConfig extractFileUploadConfig(JsonNode root) { continue; } - // Extract field ID JsonNode idNode = field.get("id"); if (idNode == null || !idNode.isTextual()) continue; + String fieldName = idNode.asText().trim(); - if (fieldName.isEmpty()) continue; - - Set acceptedTypes = new HashSet<>(); - - // Extract accepted file types - JsonNode acceptedFileTypesNode = field.get("acceptedFileTypes"); - if (acceptedFileTypesNode != null) { - if (acceptedFileTypesNode.isArray()) { - for (JsonNode typeNode : acceptedFileTypesNode) { - if (typeNode.isTextual()) { - String trimmed = typeNode.asText().trim(); - if (!trimmed.isEmpty()) { - acceptedTypes.add(trimmed); - } - } - } - } else if (acceptedFileTypesNode.isTextual()) { - Arrays.stream(acceptedFileTypesNode.asText().split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .forEach(acceptedTypes::add); + if (!fieldName.equals(targetFieldName)) continue; + + // Found the target field - extract accepted types + return extractAcceptedTypes(field.get("acceptedFileTypes")); + } + + return Collections.emptySet(); + } + + private static Set extractAcceptedTypes(JsonNode acceptedFileTypesNode) { + if (acceptedFileTypesNode == null) return Collections.emptySet(); + + Set acceptedTypes = new HashSet<>(); + + if (acceptedFileTypesNode.isArray()) { + for (JsonNode typeNode : acceptedFileTypesNode) { + if (typeNode.isTextual()) { + String trimmed = typeNode.asText().trim(); + if (!trimmed.isEmpty()) acceptedTypes.add(trimmed); } } - - fieldAcceptedTypes.put(fieldName, acceptedTypes); + } else if (acceptedFileTypesNode.isTextual()) { + Arrays.stream(acceptedFileTypesNode.asText().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(acceptedTypes::add); } - return new FileTypeConfig(fieldAcceptedTypes); + return acceptedTypes.isEmpty() ? Collections.emptySet() : acceptedTypes; } } \ No newline at end of file diff --git a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java index d5a4158f..38c54e2e 100644 --- a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java +++ b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java @@ -1955,10 +1955,9 @@ public void uploadFile_withUnrecognizedFileType_throwsInvalidFileType() throws J } @Test - public void uploadFile_withFieldNameNotInUISpec_throwsInvalidFileType() throws JsonProcessingException { + public void uploadFile_withFieldNameNotInUISpec_throwsInvalidField() throws JsonProcessingException { String transactionId = "txn-123"; String fieldName = "unknownField"; - // Valid PNG magic bytes byte[] pngBytes = new byte[]{ (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; @@ -1967,7 +1966,6 @@ public void uploadFile_withFieldNameNotInUISpec_throwsInvalidFileType() throws J RegistrationTransaction transaction = new RegistrationTransaction("user", Purpose.REGISTRATION); when(cacheUtilService.getChallengeVerifiedTransaction(transactionId)).thenReturn(transaction); - // UI spec with only "photo" field - "unknownField" is not present, so allowedTypesForField will be empty String uiSpecJson = """ { "schema": [ @@ -1978,18 +1976,16 @@ public void uploadFile_withFieldNameNotInUISpec_throwsInvalidFileType() throws J } ] } - """; + """; JsonNode uiSpecNode = objectMapper.readTree(uiSpecJson); when(profileRegistryPlugin.getUISpecification()).thenReturn(uiSpecNode); - try { - registrationService.uploadFile(transactionId, fieldName, file); - Assert.fail("Expected SignUpException to be thrown"); - } catch (SignUpException e) { - Assert.assertEquals(ErrorConstants.INVALID_FILE_TYPE, e.getErrorCode()); - } + SignUpException ex = Assert.assertThrows(SignUpException.class, + () -> registrationService.uploadFile(transactionId, fieldName, file)); + Assert.assertEquals(ErrorConstants.INVALID_FIELD, ex.getErrorCode()); } + @Test public void uploadFile_withMimeTypeNotInAllowedTypes_throwsInvalidFileType() throws JsonProcessingException { String transactionId = "txn-123"; From 4ebb3af2b7db348d19031458b80544ee549e6adf Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Wed, 28 Jan 2026 11:51:55 +0530 Subject: [PATCH 6/8] made changes as per PR review Signed-off-by: KashiwalHarsh --- .../signup/services/RegistrationService.java | 2 +- .../io/mosip/signup/util/UploadFileUtils.java | 48 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index 0cddf600..6934ebe7 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -336,7 +336,7 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { String detectedMimeType = UploadFileUtils.detectMimeType(fileBytes); - if ("application/octet-stream".equals(detectedMimeType)) { + if (UploadFileUtils.UNKNOWN_MIME_TYPE.equals(detectedMimeType)) { log.error("Unrecognized file type for field: {}", fieldName); throw new SignUpException(ErrorConstants.INVALID_FILE_TYPE); } diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java index e26dad2f..85b1742e 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -1,14 +1,13 @@ package io.mosip.signup.util; import com.fasterxml.jackson.databind.JsonNode; import java.util.*; -import java.util.stream.Collectors; +import java.util.stream.StreamSupport; public class UploadFileUtils { - + public static final String UNKNOWN_MIME_TYPE = "application/octet-stream"; private static final Map MAGIC_SIGNATURES = Map.of( "89504E47", "image/png", "FFD8FF", "image/jpeg", - "52494646", "image/webp", "25504446", "application/pdf", "504B0304", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "D0CF11E0", "application/msword" @@ -16,7 +15,7 @@ public class UploadFileUtils { public static String detectMimeType(byte[] fileBytes) { if (fileBytes == null || fileBytes.length < 4) { - return "application/octet-stream"; + return UNKNOWN_MIME_TYPE; } StringBuilder hexBuilder = new StringBuilder(); @@ -36,12 +35,12 @@ public static String detectMimeType(byte[] fileBytes) { // Check other signatures for (Map.Entry entry : MAGIC_SIGNATURES.entrySet()) { - if (hexString.startsWith(entry.getKey()) && !entry.getKey().equals("52494646")) { + if (hexString.startsWith(entry.getKey())) { return entry.getValue(); } } - return "application/octet-stream"; + return UNKNOWN_MIME_TYPE; } public static Set getAcceptedTypesForField(JsonNode root, String targetFieldName) { @@ -54,26 +53,29 @@ public static Set getAcceptedTypesForField(JsonNode root, String targetF return Collections.emptySet(); } - for (JsonNode field : schemaNode) { - JsonNode controlTypeNode = field.get("controlType"); - if (controlTypeNode == null || !controlTypeNode.isTextual()) continue; - - String controlType = controlTypeNode.asText().trim(); - if (!controlType.equalsIgnoreCase("photo") && !controlType.equalsIgnoreCase("fileUpload")) { - continue; - } - - JsonNode idNode = field.get("id"); - if (idNode == null || !idNode.isTextual()) continue; - - String fieldName = idNode.asText().trim(); - if (!fieldName.equals(targetFieldName)) continue; + return StreamSupport.stream(schemaNode.spliterator(), false) + .filter(UploadFileUtils::isFileUploadField) + .filter(field -> targetFieldName.equals(getFieldId(field))) + .findFirst() + .map(field -> extractAcceptedTypes(field.get("acceptedFileTypes"))) + .orElse(Collections.emptySet()); + } - // Found the target field - extract accepted types - return extractAcceptedTypes(field.get("acceptedFileTypes")); + private static boolean isFileUploadField(JsonNode field) { + JsonNode controlTypeNode = field.get("controlType"); + if (controlTypeNode == null || !controlTypeNode.isTextual()) { + return false; } + String controlType = controlTypeNode.asText().trim(); + return controlType.equalsIgnoreCase("photo") || controlType.equalsIgnoreCase("fileUpload"); + } - return Collections.emptySet(); + private static String getFieldId(JsonNode field) { + JsonNode idNode = field.get("id"); + if (idNode == null || !idNode.isTextual()) { + return null; + } + return idNode.asText().trim(); } private static Set extractAcceptedTypes(JsonNode acceptedFileTypesNode) { From 20d9e2327c37af603806d2f183b6774d6a228a05 Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Wed, 28 Jan 2026 19:25:32 +0530 Subject: [PATCH 7/8] added inputStream for zip based formats Signed-off-by: KashiwalHarsh --- .../signup/services/RegistrationService.java | 20 +++--- .../io/mosip/signup/util/UploadFileUtils.java | 64 ++++++++++++++++--- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index 6934ebe7..6d99fca5 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -30,6 +30,8 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; import java.util.Base64; @@ -318,15 +320,7 @@ public JsonNode getUiSpec() { } private void validateFieldAndFile(MultipartFile file, String fieldName) { - byte[] fileBytes; - try { - fileBytes = file.getBytes(); - } catch (IOException e) { - log.error("Failed to read uploaded file bytes", e); - throw new SignUpException(ErrorConstants.UPLOAD_FAILED); - } - - JsonNode uiSpec = profileRegistryPlugin.getUISpecification(); + JsonNode uiSpec = getUiSpec(); Set allowedTypesForField = UploadFileUtils.getAcceptedTypesForField(uiSpec, fieldName); if (allowedTypesForField.isEmpty()) { @@ -334,7 +328,13 @@ private void validateFieldAndFile(MultipartFile file, String fieldName) { throw new SignUpException(ErrorConstants.INVALID_FIELD); } - String detectedMimeType = UploadFileUtils.detectMimeType(fileBytes); + String detectedMimeType; + try (InputStream inputStream = file.getInputStream()) { + detectedMimeType = UploadFileUtils.detectMimeType(inputStream); + } catch (IOException e) { + log.error("Failed to read uploaded file", e); + throw new SignUpException(ErrorConstants.UPLOAD_FAILED); + } if (UploadFileUtils.UNKNOWN_MIME_TYPE.equals(detectedMimeType)) { log.error("Unrecognized file type for field: {}", fieldName); diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java index 85b1742e..58163763 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -1,28 +1,43 @@ package io.mosip.signup.util; import com.fasterxml.jackson.databind.JsonNode; +import io.mosip.signup.exception.SignUpException; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.*; import java.util.stream.StreamSupport; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class UploadFileUtils { public static final String UNKNOWN_MIME_TYPE = "application/octet-stream"; + private static final String ZIP_SIGNATURE = "504B0304"; private static final Map MAGIC_SIGNATURES = Map.of( "89504E47", "image/png", "FFD8FF", "image/jpeg", "25504446", "application/pdf", - "504B0304", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "D0CF11E0", "application/msword" ); - public static String detectMimeType(byte[] fileBytes) { - if (fileBytes == null || fileBytes.length < 4) { + public static String detectMimeType(InputStream inputStream) throws IOException { + if (inputStream == null) { return UNKNOWN_MIME_TYPE; } + // Wrap to support mark/reset for ZIP files + BufferedInputStream bis = new BufferedInputStream(inputStream); + bis.mark(Integer.MAX_VALUE); - StringBuilder hexBuilder = new StringBuilder(); - int bytesToCheck = Math.min(fileBytes.length, 12); + byte[] headerBytes = new byte[12]; + int bytesRead = bis.read(headerBytes); - for (int i = 0; i < bytesToCheck; i++) { - hexBuilder.append(String.format("%02X", fileBytes[i] & 0xFF)); + if (bytesRead < 4) { + return UNKNOWN_MIME_TYPE; + } + + StringBuilder hexBuilder = new StringBuilder(); + for (int i = 0; i < bytesRead; i++) { + hexBuilder.append(String.format("%02X", headerBytes[i] & 0xFF)); } String hexString = hexBuilder.toString(); @@ -33,6 +48,13 @@ public static String detectMimeType(byte[] fileBytes) { return "image/webp"; } + // Check for ZIP-based formats + if (hexString.startsWith(ZIP_SIGNATURE)) { + bis.reset(); + byte[] fullBytes = bis.readAllBytes(); + return detectOfficeFormat(fullBytes); + } + // Check other signatures for (Map.Entry entry : MAGIC_SIGNATURES.entrySet()) { if (hexString.startsWith(entry.getKey())) { @@ -43,6 +65,30 @@ public static String detectMimeType(byte[] fileBytes) { return UNKNOWN_MIME_TYPE; } + private static String detectOfficeFormat(byte[] fileBytes) { + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String entryName = entry.getName(); + + // DOCX: contains word/document.xml + if ("word/document.xml".equalsIgnoreCase(entryName)) { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } + // PPTX: contains ppt/presentation.xml + if ("ppt/presentation.xml".equalsIgnoreCase(entryName)) { + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + } + + zis.closeEntry(); + } + } catch (IOException ignored) { + throw new SignUpException(ErrorConstants.UPLOAD_FAILED); + } + return UNKNOWN_MIME_TYPE; + } + + public static Set getAcceptedTypesForField(JsonNode root, String targetFieldName) { if (root == null || targetFieldName == null || targetFieldName.isBlank()) { return Collections.emptySet(); @@ -86,13 +132,13 @@ private static Set extractAcceptedTypes(JsonNode acceptedFileTypesNode) if (acceptedFileTypesNode.isArray()) { for (JsonNode typeNode : acceptedFileTypesNode) { if (typeNode.isTextual()) { - String trimmed = typeNode.asText().trim(); + String trimmed = typeNode.asText().trim().toLowerCase(Locale.ROOT); if (!trimmed.isEmpty()) acceptedTypes.add(trimmed); } } } else if (acceptedFileTypesNode.isTextual()) { Arrays.stream(acceptedFileTypesNode.asText().split(",")) - .map(String::trim) + .map(s -> s.trim().toLowerCase(Locale.ROOT)) .filter(s -> !s.isEmpty()) .forEach(acceptedTypes::add); } From 6ada3ee06a773a73f21e1f0e8564b5e9ec64acd4 Mon Sep 17 00:00:00 2001 From: KashiwalHarsh Date: Fri, 30 Jan 2026 17:39:04 +0530 Subject: [PATCH 8/8] added bufferedStream directly to ZipInputStream Signed-off-by: KashiwalHarsh --- .../main/java/io/mosip/signup/util/UploadFileUtils.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java index 58163763..98cf16f9 100644 --- a/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java +++ b/signup-service/src/main/java/io/mosip/signup/util/UploadFileUtils.java @@ -51,8 +51,7 @@ public static String detectMimeType(InputStream inputStream) throws IOException // Check for ZIP-based formats if (hexString.startsWith(ZIP_SIGNATURE)) { bis.reset(); - byte[] fullBytes = bis.readAllBytes(); - return detectOfficeFormat(fullBytes); + return detectOfficeFormat(bis); } // Check other signatures @@ -65,8 +64,8 @@ public static String detectMimeType(InputStream inputStream) throws IOException return UNKNOWN_MIME_TYPE; } - private static String detectOfficeFormat(byte[] fileBytes) { - try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { + private static String detectOfficeFormat(InputStream inputStream) { + try (ZipInputStream zis = new ZipInputStream(inputStream)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { String entryName = entry.getName();