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: