Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Best Practices to Secure Web Applications

Best Practices to Secure Web Applications

While strong authentication and authorization lay the foundation, achieving robust web application security demands a multi-layered approach. At first glance, the field of web application security can seem daunting, but by understanding and implementing core principles, you can dramatically strengthen your defenses against cyber threats.

In this presentation, we'll delve into the essential, yet often neglected, aspects of securing your applications. We'll cover best practices for secure coding, meticulous input validation techniques, the importance of strategic error handling and logging, how to manage file uploads safely, and much more.

Loiane Groner

June 26, 2024
Tweet

More Decks by Loiane Groner

Other Decks in Programming

Transcript

  1. Application Security Plan Development Build Test QA / UAT Production

    Security Testing Production Delays due to late assessment Worse: assessment is done after production every X months Source: NIST Ponemom Institute
  2. Shift left Plan Development Build Test QA / UAT Production

    Source: NIST Ponemom Institute Remediation Costs:
  3. OWASP Top 10 API Security Risks – 2023 Broken Object

    Level Authorization Unrestricted Access to Sensitive Business Flows Broken Authentication Server Side Request Forgery Broken Object Property Level Authorization Security Misconfiguration Unrestricted Resource Consumption Improper Inventory Management Broken Function Level Authorization Unsafe Consumption of APIs 1 2 3 4 5 6 7 8 9 10 https://owasp.org/API-Security/editions/2023/en/0x11-t10/
  4. Bad Practice: Checking not allowed public CourseDTO update(@PathVariable Long id,

    @RequestBody Course course) { User user = getAuthenticatedUser(); if (user.getRole().equals(Role.STUDENT)) { throw new AccessDeniedException("You don't have permission to update course"); } return courseService.update(id, course); }
  5. Good Practice: Deny by Default @PutMapping(value = "/{id}") public CourseDTO

    update(@PathVariable Long id, @RequestBody Course course) { User user = getAuthenticatedUser(); // allow to update if role is admin or teacher if (user.getRole().equals(Role.ADMIN) || user.getRole().equals(Role.TEACHER)) { return courseService.update(id, course); } throw new AccessDeniedException("You don't have permission to update course"); }
  6. Potential Problem: Role Explosion @PreAuthorize("hasRole('ADMIN') or hasRole('TEACHER') or hasRole('TEACHING_ASSISTANT') or

    hasRole('ACCOUNT_MANAGER') ") public CourseDTO update(@PathVariable Long id, @RequestBody Course course) { return courseService.update(id, course); }
  7. Multiple roles and permissions: Course View Enrol Create Edit Details

    Add Lessons Delete Student ✅ ✅ ❌ ❌ ❌ ❌ Teacher ✅ ❌ ✅ ✅ ✅ ❌ Teaching Assistant ✅ ❌ ❌ ❌ ✅ ❌ Account Manager ✅ ❌ ❌ ✅ ✅ ✅ Admin ✅ ❌ ✅ ✅ ✅ ✅
  8. Decouple from the logic: custom security expression private boolean hasPrivilege(Authentication

    auth, String targetType, String permission) { for (GrantedAuthority grantedAuth : auth.getAuthorities()) { if (grantedAuth.getAuthority().startsWith(targetType) && grantedAuth.getAuthority().contains(permission)) { return true; } } return false; } public class User{ private String username; private String password; private Set<Privilege> privileges; } public class Privilege { private String name; }
  9. Decouple from the logic: custom security expression No more hardcoding!

    @PreAuthorize("hasPrivilege('COURSE_UPDATE_PRIVILEGE')") public CourseDTO update(@PathVariable Long id, @RequestBody Course course) { return courseService.update(id, course); }
  10. Should a teacher be able to update ANY course? @PreAuthorize("hasAuthority('COURSE_UPDATE_PRIVILEGE')")

    public CourseDTO update(@PathVariable Long id, @RequestBody Course course) { return courseService.update(id, course); } Checks only if user is authorized Can update any course as long as you know the ID? Deny by default!
  11. Broken Object Level Authorization public CourseDTO update(@PathVariable Long id, @RequestBody

    Course course) { if (canUpdateCourse(getAuthenticatedUser(), course)) { return courseService.update(id, course); } throw new AccessDeniedException("You do not have permission"); } public static canUpdateCourse(User user, Course course) { if (isAdmin(user) or isAccountManager(user)) return true; // Teacher can update course if they are the teacher of the course if (isTeacher(user) and course.getTeacher().getId() == user.getId()) return true; } From the request!
  12. Never trust the request! public CourseDTO update(@PathVariable Long id, @RequestBody

    Course course) { Course courseFromDB = courseService.findById(id); if (canUpdateCourse(getAuthenticatedUser(), courseFromDB)) { return courseService.update(id, course); } throw new AccessDeniedException("You do not have permission."); }
  13. Avoid exposing entities @GetMapping("/loggedInUser") public User getMyInfo() { return userService.getMyInfo();

    } @Entity public class User { @Id private String userName; private String password; @JsonIgnore public String getPassword() { return password; } }
  14. Avoid exposing entities @Entity public class User { @Id private

    String userName; private String password; private String ssn; // .. } @JsonIgnore public String getPassword() { return password; } public String getSsn() { return ssn; }
  15. Create DTOs public record UserDTO(String userName) {} @GetMapping("/api/v1/loggedInUser") public UserDTO

    getMyInfo() { User user = userService.getMyInfo(); return new UserDTO(user.getUserName()); }
  16. DTO for request and response? Be careful! public record StudentDTO(Long

    id, String userName, double averageGrade) {} @PutMapping("/students/{id}") public StudentDTO update(@PathVariable Long id, @RequestBody StudentDTO studentDTO) { // check for access rights Student student = studentRepository.findById(id).orElseThrow(); student.setUserName(studentDTO.userName()); student.setAverageGrade(studentDTO.averageGrade()); studentRepository.save(student); } Make sure to ignore the studentDTO id for updates if reusing DTO
  17. Lower environment exposed # spring jpa database MySQL for Dev

    enviroment spring.datasource.url=jdbc:mysql://CompanyDEV:3306/learningPlatform?useSSL=false spring.datasource.username=learningPlatformUser spring.datasource.password=learningPlatform@Dev12345! spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.hibernate.ddl-auto=update Prefer to use a vault solution to retrieve the password
  18. Lower environment exposed # spring jpa database MySQL for Dev

    enviroment spring.datasource.url=jdbc:mysql://CompanyDEV:3306/learningPlatform?useSSL=false spring.datasource.username=learningPlatformUser spring.datasource.password=learningPlatform@Dev12345! spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.hibernate.ddl-auto=update Don't use user with DDL access
  19. Front-end: fully validated! this.form = this.formBuilder.group({ name: [ course.name, [Validators.required,

    Validators.minLength(5), Validators.maxLength(100)]], category: [course.category, [Validators.required]], lessons: this.formBuilder.array(this.retrieveLessons(course), Validators.required) });
  20. Is the data validated on the server side? @PostMapping @ResponseStatus(code

    = HttpStatus.CREATED) public CourseDTO create(@RequestBody CourseRequestDTO course) { return courseService.create(course); } public record CourseRequestDTO( String name, String category, List<LessonDTO> lessons ) {}
  21. Same validations on the front-end and API this.form = this.formBuilder.group({

    name: [ course.name, [Validators.required, Validators.minLength(5), Validators.maxLength(100)]], category: [course.category, [Validators.required]], lessons: this.formBuilder.array( this.retrieveLessons(course), Validators.required) }); public record CourseRequestDTO( @NotBlank @NotNull @Length(min = 5, max = 100) String name, @NotBlank @NotNull @ValueOfEnum(enumClass = Category.class) String category, @NotNull @NotEmpty @Valid List<LessonDTO> lessons) {}
  22. Validate type, length, format, and range, and enforce limits @NotBlank

    @NotNull @Length(min = 5, max = 100) @Column(length = 100, nullable = false) private String name; @NotNull @Column(length = 10, nullable = false) @Convert(converter = CategoryConverter.class) private Category category; @NotNull @Column(length = 8, nullable = false) @Convert(converter = StatusConverter.class) private Status status = Status.ACTIVE; @NotNull @NotEmpty @Valid @OneToMany(mappedBy = "course" …) private Set<Lesson> lessons = new HashSet<>();
  23. Validate Strings! @NotBlank @NotNull @Length(min = 5, max = 100)

    @Column(length = 100, nullable = false) private String name; POST http://127.0.0.1:8080/api/courses content-type: application/json { "name": "!@#$%ˆ&*();" } Is this a valid name?
  24. Prefer allowed lists @Pattern(regexp = "^[^!@#$%^&*()_+]+$") private String name; @Pattern(regexp

    = "^[a-zA-Z0-9-_() ]+$") private String name; Prefer allowed list
  25. Secure all the layers! @Validated public class CourseController { public

    CoursePageDTO findAll(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") @Max(1000) int pageSize) { } public List<CourseDTO> findByName(@RequestParam @NotNull @NotBlank @Pattern(regexp = "^[a-zA-Z0-9-_() ]+$") String name) { } public CourseDTO findById(@PathVariable @Positive @NotNull Long id) { } public CourseDTO create(@RequestBody @Valid CourseRequestDTO course) { } public CourseDTO update(@PathVariable @Positive @NotNull Long id, @RequestBody @Valid CourseRequestDTO course) { } } Sanitize or use regex
  26. Secure all the layers! @Validated public class CourseService { public

    CoursePageDTO findAll(@PositiveOrZero int page, @Positive @Max(1000) int pageSize) {} public List<CourseDTO> findByName(@NotNull @NotBlank @Pattern(regexp = "^[a-zA-Z0-9-_() ]+$") String name) {} //... }
  27. Secure all the layers! public class Course { @NotBlank @NotNull

    @Length(min = 5, max = 100) @Column(length = 100, nullable = false) @Pattern(regexp = "^[a-zA-Z0-9-_() ]+$") private String name; @NotNull @Column(length = 10, nullable = false) @Convert(converter = CategoryConverter.class) private Category category; @NotNull @Column(length = 8, nullable = false) @Convert(converter = StatusConverter.class) private Status status = Status.ACTIVE; }
  28. SQL Injection - dynamic SQL String query = "from Course

    where name = " + courseName; Query q = sessionFactory.getCurrentSession().createQuery(query); List<Course> courses = q.list(); String query = "from Course where name = :name"; Query q = sessionFactory.getCurrentSession().createQuery(query); q.setParameter("name", userName); List<Course> courses = q.list();
  29. Extension and Type Validation # Using Apache Commons IO. Returns

    "pdf" import org.apache.commons.io.FilenameUtils; String extension = FilenameUtils.getExtension("test.pdf"); Don't trust the Content-Type header, always go to the data source! Set a filename length limit and restrict the allowed characters if possible
  30. Path Traversal Vulnerability @PostMapping("/uploadThumbnail") public void uploadThumbnail (@RequestParam MultipartFile file)

    throws IOException { var name = file.getOriginalFilename().replace(" ", "_"); var fileNameAndPath = Paths.get(STORAGE_DIRECTORY, name); Files.write(fileNameAndPath, file.getBytes()); } What if fileName gets updated to images/profiles/../../../../../../../etc/passwd?
  31. Apache Commons IO import org.apache.commons.io.FilenameUtils; @PostMapping("/uploadThumbnail") public void uploadThumbnail (@RequestParam

    MultipartFile file) throws IOException { var name = file.getOriginalFilename().replace(" ", "_"); var fileNameAndPath = Paths.get(STORAGE_DIRECTORY, name); String normalizedFileName = FilenameUtils.normalize(fileNameAndPath); Files.write(normalizedFileName, file.getBytes()); }
  32. Apache Tika public void uploadThumbnail(@RequestParam MultipartFile file) throws IOException {

    try (InputStream stream = TikaInputStream.get(file.getInputStream())) { AutoDetectParser parser = new AutoDetectParser(); BodyContentHandler handler = new BodyContentHandler(); Metadata metadata = new Metadata(); parser.parse(stream, handler, metadata); String normalizedFileName = metadata.get(Metadata.RESOURCE_NAME_KEY); Files.write(normalizedFileName, file.getBytes()); } catch (Exception e) { throw new IOException("Failed to normalize file name", e); } }
  33. File Content Validation • Scan the file for viruses •

    Check for macros and formulas in CSVs and Excel spreadsheets • Check of embedded objects in documents
  34. Do not expose the stack trace with Exceptions server.error.include-stacktrace=never Log

    the stack trace, always return a generic message that is helpful
  35. Careful with what you are logging Authentication Credentials Personally Identifiable

    Information (PII) Financial Information Health and Medical Information Confidential Business Data Legal and Compliance Data Personal Communications Biometric Data
  36. Remove sensitive data from toString class User { private String

    userid; private String password; public String toString(){ return "Userid: " + userid; } } logger.info("User logged in: " + user.toString());
  37. Use IDs, vault tokens or masked data instead logger.info("Payment submitted

    for credit card number: " + creditCard.getNumber()); logger.info("Payment submitted for credit card id: " + creditCard.getId());
  38. Rate limit for APIs • Spring AOP for simple solutions

    • Bucket4j Rate Limiting Library • Redis Also helps preventing data mining in case one API has vulnerabilities
  39. Unit Tests are not enough! Test the exception cases! @ParameterizedTest(name

    = "should return bad request when creating a course with invalid data: {0}") @MethodSource("com.loiane.course.TestData#createInvalidCoursesDTO") void testCreateInvalid(CourseRequestDTO course) { assertThrows(ConstraintViolationException.class, () -> this.courseService.create(course)); then(courseRepository).shouldHaveNoInteractions(); } public static List<CourseRequestDTO> createInvalidCoursesDTO() { return List.of( new CourseRequestDTO(null, null, createLessonsDTO()), new CourseRequestDTO(VALID_CATEGORY, null, createLessonsDTO()), new CourseRequestDTO(VALID_CATEGORY, "", createLessonsDTO()), new CourseRequestDTO(VALID_CATEGORY, INVALID_COURSE_NAME, createLessonsDTO()), new CourseRequestDTO(VALID_CATEGORY, LOREN_IPSUM, createLessonsDTO()), new CourseRequestDTO(null, VALID_NAME, createLessonsDTO()), // more invalid data }
  40. Educate the team • Have internal demos of the product

    • Ask questions how this was implemented • Give feedback • Make sure security is part of requirements and user stories • Security should not be treated as technical debt • Prioritize the vulnerabilities often found within your organization
  41. Simple security checklist All endpoints are authenticated and authorized? Can

    user modify the data / record? Input is validated and sanitized? Validation in all layers? Entity, Service, Controller? Validation done against data source instead of request? File uploads validated: extension, fileName, formulas/macros/objects, scanned? No passwords / keys / confidential data exposed? Error handling not exposing the stack trace?