Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@
<artifactId>json-path</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand All @@ -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());

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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,Object> 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)");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> ALLOWED_EXTENSIONS = Arrays.asList(".png", ".jpg", ".jpeg");
public static final List<String> 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,Object> imageComponents(String base64Data) throws IOException, MimeTypeException {
Map<ImageComponents,Object> 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));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.park.utmstack.util.enums;

public enum ImageComponents {
IMG_MIME_TYPE,
IMG_FILE_EXTENSION,
IMG_DATA

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.park.utmstack.util.exceptions;

public class UtmImageValidationException extends RuntimeException {
public UtmImageValidationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -42,14 +47,19 @@ public UtmImagesResource(UtmImagesService utmImagesService,
public ResponseEntity<UtmImages> updateImage(@Valid @RequestBody UtmImages image) {
final String ctx = CLASSNAME + ".updateImage";
try {
UtmImages result = utmImagesService.save(image);
return ResponseEntity.ok(result);
Optional<UtmImages> 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);
}
}

Expand All @@ -63,7 +73,7 @@ public ResponseEntity<List<UtmImages>> 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);
}
}

Expand All @@ -78,7 +88,7 @@ public ResponseEntity<UtmImages> 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);
}
}

Expand All @@ -93,7 +103,7 @@ public ResponseEntity<Void> 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);
}
}
}
2 changes: 1 addition & 1 deletion backend/src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: