diff --git a/backend/pom.xml b/backend/pom.xml index 9f708b107..3b3ef9ac1 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -349,6 +349,11 @@ json-path 2.8.0 + + org.apache.tika + tika-core + 2.9.1 + diff --git a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java index 07fd2cc4b..b127ae88d 100644 --- a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java @@ -24,6 +24,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletResponse; @@ -39,24 +41,26 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; private final CorsFilter corsFilter; private final InternalApiKeyProvider internalApiKeyProvider; + private final JHipsterProperties jHipsterProperties; public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService, TokenProvider tokenProvider, - CorsFilter corsFilter, InternalApiKeyProvider internalApiKeyProvider) { + CorsFilter corsFilter, InternalApiKeyProvider internalApiKeyProvider, JHipsterProperties jHipsterProperties) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; this.corsFilter = corsFilter; this.internalApiKeyProvider = internalApiKeyProvider; + this.jHipsterProperties = jHipsterProperties; } @PostConstruct public void init() { try { authenticationManagerBuilder - .userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder()); + .userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder()); } catch (Exception e) { throw new BeanInitializationException("Security configuration failed", e); } @@ -76,51 +80,53 @@ public PasswordEncoder passwordEncoder() { @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring() - .antMatchers(HttpMethod.OPTIONS, "/**") - .antMatchers("/swagger-ui/**") - .antMatchers("/i18n/**"); + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/swagger-ui/**") + .antMatchers("/i18n/**"); } @Override public void configure(HttpSecurity http) throws Exception { http - .csrf() - .disable() - .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling() - .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) - .accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN)) - .and() - .headers() - .frameOptions() - .disable() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .authorizeRequests() - .antMatchers("/api/authenticate").permitAll() - .antMatchers("/api/authenticateFederationServiceManager").permitAll() - .antMatchers("/api/ping").permitAll() - .antMatchers("/api/date-format").permitAll() - .antMatchers("/api/healthcheck").permitAll() - .antMatchers("/api/releaseInfo").permitAll() - .antMatchers("/api/account/reset-password/init").permitAll() - .antMatchers("/api/account/reset-password/finish").permitAll() - .antMatchers("/api/images/all").permitAll() - .antMatchers("/api/tfa/verifyCode").hasAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER) - .antMatchers("/api/utm-incident-jobs").hasAuthority(AuthoritiesConstants.ADMIN) - .antMatchers("/api/utm-incident-jobs/**").hasAuthority(AuthoritiesConstants.ADMIN) - .antMatchers("/api/custom-reports/**").denyAll() - .antMatchers("/api/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) - .antMatchers("/ws/topic").hasAuthority(AuthoritiesConstants.ADMIN) - .antMatchers("/ws/**").permitAll() - .antMatchers("/management/info").permitAll() - .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) - .and() - .apply(securityConfigurerAdapterForJwt()) - .and() - .apply(securityConfigurerAdapterForInternalApiKey()); + .csrf() + .disable() + .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling() + .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) + .accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN)) + .and() + .headers() + .contentSecurityPolicy(jHipsterProperties.getSecurity().getContentSecurityPolicy()) + .and() + .frameOptions() + .disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/authenticate").permitAll() + .antMatchers("/api/authenticateFederationServiceManager").permitAll() + .antMatchers("/api/ping").permitAll() + .antMatchers("/api/date-format").permitAll() + .antMatchers("/api/healthcheck").permitAll() + .antMatchers("/api/releaseInfo").permitAll() + .antMatchers("/api/account/reset-password/init").permitAll() + .antMatchers("/api/account/reset-password/finish").permitAll() + .antMatchers("/api/images/all").permitAll() + .antMatchers("/api/tfa/verifyCode").hasAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER) + .antMatchers("/api/utm-incident-jobs").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/api/utm-incident-jobs/**").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/api/custom-reports/**").denyAll() + .antMatchers("/api/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) + .antMatchers("/ws/topic").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/ws/**").permitAll() + .antMatchers("/management/info").permitAll() + .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) + .and() + .apply(securityConfigurerAdapterForJwt()) + .and() + .apply(securityConfigurerAdapterForInternalApiKey()); } diff --git a/backend/src/main/java/com/park/utmstack/service/UtmImagesService.java b/backend/src/main/java/com/park/utmstack/service/UtmImagesService.java index 0ef6b800e..635c18d4f 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmImagesService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmImagesService.java @@ -3,13 +3,18 @@ import com.park.utmstack.domain.UtmImages; import com.park.utmstack.domain.shared_types.enums.ImageShortName; import com.park.utmstack.repository.UtmImagesRepository; +import com.park.utmstack.util.enums.ImageComponents; +import com.park.utmstack.util.ImageValidatorUtil; +import com.park.utmstack.util.exceptions.UtmImageValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.InputStream; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -35,9 +40,32 @@ public UtmImagesService(UtmImagesRepository utmImagesRepository) { * @param utmImages the entity to save * @return the persisted entity */ - public UtmImages save(UtmImages utmImages) { + public UtmImages save(UtmImages utmImages) throws UtmImageValidationException { log.debug("Request to save UtmImages : {}", utmImages); - return utmImagesRepository.save(utmImages); + try { + Map imageComponents = ImageValidatorUtil.imageComponents(utmImages.getUserImg()); + if(imageComponents.isEmpty()) { + throw new NullPointerException("Map is empty"); + } + String mimeType = (String) imageComponents.get(ImageComponents.IMG_MIME_TYPE); + String extension = (String) imageComponents.get(ImageComponents.IMG_FILE_EXTENSION); + InputStream imageData = (InputStream) imageComponents.get(ImageComponents.IMG_DATA); + + if (!ImageValidatorUtil.isMimeTypeAllowed(mimeType)) { + throw new UtmImageValidationException("Could not update UtmImages: Invalid image MIME_TYPE (" + mimeType + ") only (" + String.join(",", ImageValidatorUtil.ALLOWED_MIME_TYPES) + ") MIME_TYPES are allowed"); + } + if (!ImageValidatorUtil.isExtensionAllowed(extension)) { + throw new UtmImageValidationException("Could not update UtmImages: Invalid image extension (" + extension + ") only (" + String.join(",", ImageValidatorUtil.ALLOWED_EXTENSIONS) + ") extensions are allowed"); + } + if (!ImageValidatorUtil.isRealImage(imageData)) { + throw new UtmImageValidationException("Could not update UtmImages: Not a valid image"); + } + return utmImagesRepository.save(utmImages); + } catch (UtmImageValidationException e) { + throw new UtmImageValidationException(e.getMessage()); + } catch (Exception e) { + throw new UtmImageValidationException("Could not insert the image: The image is corrupted, is not a valid image or maybe is a SVG (SVG is not allowed)"); + } } /** diff --git a/backend/src/main/java/com/park/utmstack/util/ImageValidatorUtil.java b/backend/src/main/java/com/park/utmstack/util/ImageValidatorUtil.java new file mode 100644 index 000000000..1dc92feab --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/ImageValidatorUtil.java @@ -0,0 +1,79 @@ +package com.park.utmstack.util; + +import com.park.utmstack.util.enums.ImageComponents; +import org.apache.tika.Tika; +import org.apache.tika.mime.MimeType; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypes; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.io.IOException; +import java.util.*; + +public class ImageValidatorUtil { + + public static final List ALLOWED_EXTENSIONS = Arrays.asList(".png", ".jpg", ".jpeg"); + public static final List ALLOWED_MIME_TYPES = Arrays.asList("image/png", "image/jpeg", "image/jpg"); + private static final Tika tika = new Tika(); + + /** + * Method to extract base64 data part + * */ + private static String extractBase64Data(String base64Image) { + return base64Image.split(",")[1]; // Extract base64 data part + } + + /** + * Returns the extension for a given mimetype + * */ + private static String getExtensionFromMime(String mimeType) throws MimeTypeException { + MimeTypes allTypes = MimeTypes.getDefaultMimeTypes(); + MimeType type = allTypes.forName(mimeType); + return type.getExtension(); + } + + /** + * Method to check if the image data is a real image to avoid XSS attack + * */ + public static boolean isRealImage(InputStream t) throws IOException { + BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(t.readAllBytes())); + return bufferedImage != null; + } + + /** + * Receives image data in base 64 and return map with: mimeType, fileExtension, inputStream + * The corresponding keys used are in the enum ImageComponents + * */ + public static Map imageComponents(String base64Data) throws IOException, MimeTypeException { + Map components = new LinkedHashMap<>(); + byte[] imageData = Base64.getDecoder().decode(extractBase64Data(base64Data)); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData)) { + + String mimeType = tika.detect(inputStream); + String fileExtension = getExtensionFromMime(mimeType); + + components.put(ImageComponents.IMG_MIME_TYPE,mimeType); + components.put(ImageComponents.IMG_FILE_EXTENSION,fileExtension); + components.put(ImageComponents.IMG_DATA,inputStream); + } + return components; + } + + /** + * Method to check if extension is allowed + * */ + public static boolean isExtensionAllowed(String extension) { + return ALLOWED_EXTENSIONS.contains(extension.toLowerCase(Locale.ROOT)); + } + + /** + * Method to check if mimetype is allowed + * */ + public static boolean isMimeTypeAllowed(String mimeType) { + return ALLOWED_MIME_TYPES.contains(mimeType.toLowerCase(Locale.ROOT)); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/util/enums/ImageComponents.java b/backend/src/main/java/com/park/utmstack/util/enums/ImageComponents.java new file mode 100644 index 000000000..c03894625 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/enums/ImageComponents.java @@ -0,0 +1,8 @@ +package com.park.utmstack.util.enums; + +public enum ImageComponents { + IMG_MIME_TYPE, + IMG_FILE_EXTENSION, + IMG_DATA + +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/UtmImageValidationException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/UtmImageValidationException.java new file mode 100644 index 000000000..8e9e2b570 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/UtmImageValidationException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class UtmImageValidationException extends RuntimeException { + public UtmImageValidationException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java b/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java index 818c10758..d45720ba5 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java @@ -3,8 +3,10 @@ import com.park.utmstack.domain.UtmImages; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.shared_types.enums.ImageShortName; +import com.park.utmstack.repository.UtmImagesRepository; import com.park.utmstack.service.UtmImagesService; import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.UtilResponse; import com.park.utmstack.web.rest.util.HeaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,11 +31,14 @@ public class UtmImagesResource { private static final String CLASSNAME = "UtmImagesResource"; private final UtmImagesService utmImagesService; + private final UtmImagesRepository imagesRepository; private final ApplicationEventService applicationEventService; public UtmImagesResource(UtmImagesService utmImagesService, + UtmImagesRepository imagesRepository, ApplicationEventService applicationEventService) { this.utmImagesService = utmImagesService; + this.imagesRepository = imagesRepository; this.applicationEventService = applicationEventService; } @@ -42,14 +47,19 @@ public UtmImagesResource(UtmImagesService utmImagesService, public ResponseEntity updateImage(@Valid @RequestBody UtmImages image) { final String ctx = CLASSNAME + ".updateImage"; try { - UtmImages result = utmImagesService.save(image); - return ResponseEntity.ok(result); + Optional imageOpt = imagesRepository.findById(image.getShortName()); + + if (imageOpt.isEmpty()) + return UtilResponse.buildBadRequestResponse("Image short name not recognized: " + image.getShortName()); + + UtmImages img = imageOpt.get(); + img.setUserImg(image.getUserImg()); + return ResponseEntity.ok(utmImagesService.save(img)); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert(null, null, msg)).body(null); + return UtilResponse.buildInternalServerErrorResponse(msg); } } @@ -63,7 +73,7 @@ public ResponseEntity> getAllImages() { log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert(null, null, msg)).body(null); + HeaderUtil.createFailureAlert(null, null, msg)).body(null); } } @@ -78,7 +88,7 @@ public ResponseEntity getImage(@PathVariable ImageShortName shortName log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert(null, null, msg)).body(null); + HeaderUtil.createFailureAlert(null, null, msg)).body(null); } } @@ -93,7 +103,7 @@ public ResponseEntity reset() { log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert(null, null, msg)).body(null); + HeaderUtil.createFailureAlert(null, null, msg)).body(null); } } } diff --git a/backend/src/main/resources/config/application.yml b/backend/src/main/resources/config/application.yml index 2612b4445..b7edb2e6b 100644 --- a/backend/src/main/resources/config/application.yml +++ b/backend/src/main/resources/config/application.yml @@ -127,6 +127,6 @@ jhipster: license: unlicensed license-url: security: - content-security-policy: "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" + content-security-policy: "default-src 'self' https://fonts.googleapis.com/css* https://fonts.gstatic.com/s/poppins/v20*; frame-src 'self' data:; script-src 'self' https://storage.googleapis.com; style-src 'self'; img-src 'self' data:; font-src 'self' data:" # application: