Slide 1

Slide 1 text

Best Practices to Secure Web Applications Loiane Groner @loiane

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Shift left Plan Development Build Test QA / UAT Production Source: NIST Ponemom Institute Remediation Costs:

Slide 4

Slide 4 text

Security from day 1

Slide 5

Slide 5 text

Loiane Groner Development / Engineering Manager | Published Author @loiane loiane loianegroner loiane.com

Slide 6

Slide 6 text

What is API security?

Slide 7

Slide 7 text

Authentication and Authorization

Slide 8

Slide 8 text

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/

Slide 9

Slide 9 text

Better Authorization

Slide 10

Slide 10 text

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); }

Slide 11

Slide 11 text

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"); }

Slide 12

Slide 12 text

Role-based access control @PreAuthorize("hasRole('ADMIN') or hasRole('TEACHER')") public CourseDTO update(@PathVariable Long id, @RequestBody Course course) { return courseService.update(id, course); }

Slide 13

Slide 13 text

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); }

Slide 14

Slide 14 text

Multiple roles and permissions: Course View Enrol Create Edit Details Add Lessons Delete Student ✅ ✅ ❌ ❌ ❌ ❌ Teacher ✅ ❌ ✅ ✅ ✅ ❌ Teaching Assistant ✅ ❌ ❌ ❌ ✅ ❌ Account Manager ✅ ❌ ❌ ✅ ✅ ✅ Admin ✅ ❌ ✅ ✅ ✅ ✅

Slide 15

Slide 15 text

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 privileges; } public class Privilege { private String name; }

Slide 16

Slide 16 text

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); }

Slide 17

Slide 17 text

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!

Slide 18

Slide 18 text

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!

Slide 19

Slide 19 text

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."); }

Slide 20

Slide 20 text

Property Level Issues

Slide 21

Slide 21 text

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; } }

Slide 22

Slide 22 text

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; }

Slide 23

Slide 23 text

Create DTOs public record UserDTO(String userName) {} @GetMapping("/api/v1/loggedInUser") public UserDTO getMyInfo() { User user = userService.getMyInfo(); return new UserDTO(user.getUserName()); }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Password / key exposure

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Input Validation

Slide 29

Slide 29 text

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) });

Slide 30

Slide 30 text

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 lessons ) {}

Slide 31

Slide 31 text

Never trust the input!

Slide 32

Slide 32 text

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 lessons) {}

Slide 33

Slide 33 text

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 lessons = new HashSet<>();

Slide 34

Slide 34 text

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?

Slide 35

Slide 35 text

Prefer allowed lists @Pattern(regexp = "^[^!@#$%^&*()_+]+$") private String name; Avoid

Slide 36

Slide 36 text

Prefer allowed lists @Pattern(regexp = "^[^!@#$%^&*()_+]+$") private String name; @Pattern(regexp = "^[a-zA-Z0-9-_() ]+$") private String name; Prefer allowed list

Slide 37

Slide 37 text

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 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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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; }

Slide 40

Slide 40 text

SQL Injection https://xkcd.com/327/

Slide 41

Slide 41 text

SQL Injection - dynamic SQL String query = "from Course where name = " + courseName; Query q = sessionFactory.getCurrentSession().createQuery(query); List courses = q.list(); String query = "from Course where name = :name"; Query q = sessionFactory.getCurrentSession().createQuery(query); q.setParameter("name", userName); List courses = q.list();

Slide 42

Slide 42 text

File Upload

Slide 43

Slide 43 text

Upload and Download limits spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB Simple config in Spring apps

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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?

Slide 46

Slide 46 text

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()); }

Slide 47

Slide 47 text

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); } }

Slide 48

Slide 48 text

File Content Validation ● Scan the file for viruses ● Check for macros and formulas in CSVs and Excel spreadsheets ● Check of embedded objects in documents

Slide 49

Slide 49 text

Exception Handling and logging

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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());

Slide 53

Slide 53 text

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());

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Testing

Slide 56

Slide 56 text

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 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 }

Slide 57

Slide 57 text

In my AI era!

Slide 58

Slide 58 text

Prompt Engineering

Slide 59

Slide 59 text

Sanitize the prompt input!

Slide 60

Slide 60 text

Use AI tools as an ally

Slide 61

Slide 61 text

Copilot

Slide 62

Slide 62 text

Github Actions and Security Keep dependencies up to date

Slide 63

Slide 63 text

Code Scanning

Slide 64

Slide 64 text

Education and Training

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Security Checklist for Code Reviews

Slide 67

Slide 67 text

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?

Slide 68

Slide 68 text

Thank you! loiane @loiane loiane loianegroner loiane.com