VYPR
Critical severity9.0NVD Advisory· Published Apr 20, 2026· Updated Apr 25, 2026

CVE-2026-24467

CVE-2026-24467

Description

OpenAEV is an open source platform allowing organizations to plan, schedule and conduct cyber adversary simulation campaign and tests. Starting in version 1.0.0 and prior to version 2.0.13, OpenAEV's password reset implementation contains multiple security weaknesses that together allow reliable account takeover. The primary issue is that password reset tokens do not expire. Once a token is generated, it remains valid indefinitely, even if significant time has passed or if newer tokens are issued for the same account. This allows an attacker to accumulate valid password reset tokens over time and reuse them at any point in the future to reset a victim’s password. A secondary weakness is that password reset tokens are only 8 digits long. While an 8-digit numeric token provides 100,000,000 possible combinations (which is secure enough), the ability to generate large numbers of valid tokens drastically reduces the required number of attempts to guess a valid password reset token. For example, if an attacker generates 2,000 valid tokens, the brute-force effort is reduced to approximately 50,000 attempts, which is a trivially achievable number of requests for an automated attack. (100 requests per second can mathematically find a valid password reset token in 500 seconds.) By combining these flaws, an attacker can mass-generate valid password reset tokens and then brute-force them efficiently until a match is found, allowing the attacker to reset the victim’s password to a value of their choosing. The original password is not required, and the attack can be performed entirely without authentication. This vulnerability enables full account takeover that leads to platform compromise. An unauthenticated remote attacker can reset the password of any registered user account and gain complete access without authentication. Because user email addresses are exposed to other users by design, a single guessed or observed email address is sufficient to compromise even administrator accounts with non-guessable email addresses. This design flaw results in a reliable and scalable account takeover vulnerability that affects any registered user account in the system. Note: The vulnerability does not require OpenAEV to have the email service configured. The exploit does not depend on the target email address to be a real email address. It just needs to be registered to OpenAEV. Successful exploitation allows an unauthenticated remote attacker to access sensitive data (such as the Findings section of a simulation), modify payloads executed by deployed agents to compromise all hosts where agents are installed (therefore the Scope is changed). Users should upgrade to version 2.0.13 to receive a fix.

Affected products

1
  • cpe:2.3:a:filigran:openaev:*:*:*:*:*:*:*:*
    Range: >=1.0.0,<2.0.13

Patches

1
c09a4e71ea76

[api/front] Be able to reset the password through a link on the login page (#135)

https://github.com/OpenAEV-Platform/openaevJulien RichardSep 27, 2022via nvd-ref
26 files changed · +426 107
  • openex-api/pom.xml+5 0 modified
    @@ -154,6 +154,11 @@
                 <artifactId>commons-email</artifactId>
                 <version>1.5</version>
             </dependency>
    +        <dependency>
    +            <groupId>org.apache.commons</groupId>
    +            <artifactId>commons-collections4</artifactId>
    +            <version>4.4</version>
    +        </dependency>
             <dependency>
                 <groupId>org.flywaydb</groupId>
                 <artifactId>flyway-core</artifactId>
    
  • openex-api/src/main/java/io/openex/config/AppSecurityConfig.java+1 0 modified
    @@ -63,6 +63,7 @@ protected void configure(HttpSecurity http) throws Exception {
                     /**/.antMatchers("/api/player/**").permitAll()
                     /**/.antMatchers("/api/settings").permitAll()
                     /**/.antMatchers("/api/login").permitAll()
    +                /**/.antMatchers("/api/reset/**").permitAll()
                     /**/.antMatchers("/api/**").authenticated()
                     .and()
                     .logout()
    
  • openex-api/src/main/java/io/openex/injects/challenge/ChallengeExecutor.java+1 1 modified
    @@ -79,7 +79,7 @@ public List<Expectation> process(Execution execution, ExecutableInject injection
                     List<Document> documents = injection.getInject().getDocuments().stream()
                             .filter(InjectDocument::isAttached).map(InjectDocument::getDocument).toList();
                     List<DataAttachment> attachments = resolveAttachments(execution, injection, documents);
    -                String message = content.buildMessage(injection.getInject(), imapEnabled);
    +                String message = content.buildMessage(injection, imapEnabled);
                     boolean encrypted = content.isEncrypted();
                     users.stream().parallel().forEach(userInjectContext -> {
                         try {
    
  • openex-api/src/main/java/io/openex/injects/email/EmailExecutor.java+2 2 modified
    @@ -71,7 +71,7 @@ public List<Expectation> process(Execution execution, ExecutableInject injection
             List<DataAttachment> attachments = resolveAttachments(execution, injection, documents);
             String inReplyTo = content.getInReplyTo();
             String subject = content.getSubject();
    -        String message = content.buildMessage(inject, imapEnabled);
    +        String message = content.buildMessage(injection, imapEnabled);
             boolean mustBeEncrypted = content.isEncrypted();
             // Resolve the attachments only once
             List<ExecutionContext> users = injection.getUsers();
    @@ -83,7 +83,7 @@ public List<Expectation> process(Execution execution, ExecutableInject injection
                 users = users.stream().peek(context -> context.put("document_uri", buildDocumentUri(context, inject))).toList();
             }
             Exercise exercise = injection.getSource().getExercise();
    -        String replyTo = exercise.getReplyTo();
    +        String replyTo = exercise != null ? exercise.getReplyTo() : openExConfig.getDefaultMailer();
             //noinspection SwitchStatementWithTooFewBranches
             switch (contract.getId()) {
                 case EMAIL_GLOBAL -> sendMulti(execution, users, replyTo, inReplyTo, subject, message, attachments);
    
  • openex-api/src/main/java/io/openex/injects/email/model/EmailContent.java+5 8 modified
    @@ -1,7 +1,7 @@
     package io.openex.injects.email.model;
     
     import com.fasterxml.jackson.annotation.JsonProperty;
    -import io.openex.database.model.Inject;
    +import io.openex.execution.ExecutableInject;
     import org.springframework.util.StringUtils;
     
     import java.util.Objects;
    @@ -58,24 +58,21 @@ public void setEncrypted(boolean encrypted) {
             this.encrypted = encrypted;
         }
     
    -    public String buildMessage(Inject inject, boolean imapEnabled) {
    +    public String buildMessage(ExecutableInject injection, boolean imapEnabled) {
             // String footer = inject.getFooter();
    -        String header = inject.getHeader();
    +        String header = injection.getInject().getHeader();
             StringBuilder data = new StringBuilder();
             if (StringUtils.hasLength(header)) {
                 data.append(HEADER_DIV).append(header).append(END_DIV);
             }
             data.append(START_DIV).append(body).append(END_DIV);
    -        // if (StringUtils.hasLength(footer)) {
    -        //    data.append(FOOTER_DIV).append(footer).append(END_DIV);
    -        // }
             // If imap is enable we need to inject the id marker
    -        if (imapEnabled && !inject.isDirect()) {
    +        if (injection.isRuntime() && imapEnabled) {
                 data.append(START_DIV)
                         .append("<br/><br/><br/><br/>")
                         .append("---------------------------------------------------------------------------------<br/>")
                         .append("OpenEx internal information, do not remove!<br/>")
    -                    .append("[inject_id=").append(inject.getId()).append("]<br/>")
    +                    .append("[inject_id=").append(injection.getInject().getId()).append("]<br/>")
                         .append("---------------------------------------------------------------------------------<br/>")
                         .append(END_DIV);
             }
    
  • openex-api/src/main/java/io/openex/injects/media/MediaExecutor.java+1 1 modified
    @@ -79,7 +79,7 @@ public List<Expectation> process(Execution execution, ExecutableInject injection
                         List<Document> documents = injection.getInject().getDocuments().stream()
                                 .filter(InjectDocument::isAttached).map(InjectDocument::getDocument).toList();
                         List<DataAttachment> attachments = resolveAttachments(execution, injection, documents);
    -                    String message = content.buildMessage(injection.getInject(), imapEnabled);
    +                    String message = content.buildMessage(injection, imapEnabled);
                         boolean encrypted = content.isEncrypted();
                         users.stream().parallel().forEach(userInjectContext -> {
                             try {
    
  • openex-api/src/main/java/io/openex/rest/exercise/ExerciseApi.java+7 0 modified
    @@ -1,6 +1,7 @@
     package io.openex.rest.exercise;
     
     import com.fasterxml.jackson.databind.ObjectMapper;
    +import io.openex.config.OpenExConfig;
     import io.openex.database.model.*;
     import io.openex.database.model.Exercise.STATUS;
     import io.openex.database.repository.*;
    @@ -21,6 +22,7 @@
     import org.springframework.web.multipart.MultipartFile;
     import reactor.util.function.Tuples;
     
    +import javax.annotation.Resource;
     import javax.annotation.security.RolesAllowed;
     import javax.servlet.http.HttpServletResponse;
     import javax.transaction.Transactional;
    @@ -61,6 +63,9 @@ public class ExerciseApi extends RestBehavior {
     
         @Value("${openex.mail.imap.username}")
         private String imapUsername;
    +
    +    @Resource
    +    private OpenExConfig openExConfig;
         // endregion
     
         // region repositories
    @@ -292,6 +297,8 @@ public Exercise createExercise(@Valid @RequestBody ExerciseCreateInput input) {
             exercise.setTags(fromIterable(tagRepository.findAllById(input.getTagIds())));
             if (imapEnabled) {
                 exercise.setReplyTo(imapUsername);
    +        } else {
    +            exercise.setReplyTo(openExConfig.getDefaultMailer());
             }
             // Find automatic groups to grants
             List<Group> groups = fromIterable(groupRepository.findAll());
    
  • openex-api/src/main/java/io/openex/rest/inject/form/DirectInjectInput.java+0 1 modified
    @@ -81,7 +81,6 @@ public Inject toInject() {
             inject.setDescription(getDescription());
             inject.setContent(getContent());
             inject.setContract(getContract());
    -        inject.setDirect(true);
             return inject;
         }
     }
    
  • openex-api/src/main/java/io/openex/rest/lessons/LessonsApi.java+10 48 modified
    @@ -1,26 +1,17 @@
     package io.openex.rest.lessons;
     
    -import io.openex.config.OpenExConfig;
    -import io.openex.contract.Contract;
     import io.openex.database.model.*;
     import io.openex.database.repository.*;
     import io.openex.database.specification.LessonsAnswerSpecification;
     import io.openex.database.specification.LessonsCategorySpecification;
     import io.openex.database.specification.LessonsQuestionSpecification;
    -import io.openex.execution.ExecutableInject;
    -import io.openex.execution.ExecutionContext;
    -import io.openex.execution.Injector;
    -import io.openex.injects.email.EmailContract;
    -import io.openex.injects.email.model.EmailContent;
     import io.openex.rest.helper.RestBehavior;
     import io.openex.rest.lessons.form.*;
    -import io.openex.service.ContractService;
    +import io.openex.service.MailingService;
     import org.springframework.beans.factory.annotation.Autowired;
    -import org.springframework.context.ApplicationContext;
     import org.springframework.security.access.prepost.PreAuthorize;
     import org.springframework.web.bind.annotation.*;
     
    -import javax.annotation.Resource;
     import javax.validation.Valid;
     import java.util.List;
     import java.util.Optional;
    @@ -32,19 +23,19 @@
     
     @RestController
     public class LessonsApi extends RestBehavior {
    -
    -    @Resource
    -    private OpenExConfig openExConfig;
    -
         private ExerciseRepository exerciseRepository;
         private AudienceRepository audienceRepository;
         private LessonsTemplateRepository lessonsTemplateRepository;
         private LessonsCategoryRepository lessonsCategoryRepository;
         private LessonsQuestionRepository lessonsQuestionRepository;
         private LessonsAnswerRepository lessonsAnswerRepository;
    -    private ContractService contractService;
    -    private ApplicationContext context;
         private UserRepository userRepository;
    +    private MailingService mailingService;
    +
    +    @Autowired
    +    public void setMailingService(MailingService mailingService) {
    +        this.mailingService = mailingService;
    +    }
     
         @Autowired
         public void setUserRepository(UserRepository userRepository) {
    @@ -81,16 +72,6 @@ public void setLessonsAnswerRepository(LessonsAnswerRepository lessonsAnswerRepo
             this.lessonsAnswerRepository = lessonsAnswerRepository;
         }
     
    -    @Autowired
    -    public void setContractService(ContractService contractService) {
    -        this.contractService = contractService;
    -    }
    -
    -    @Autowired
    -    public void setContext(ApplicationContext context) {
    -        this.context = context;
    -    }
    -
         @GetMapping("/api/exercises/{exerciseId}/lessons_categories")
         @PreAuthorize("isExerciseObserver(#exerciseId)")
         public Iterable<LessonsCategory> exerciseLessonsCategories(@PathVariable String exerciseId) {
    @@ -220,28 +201,9 @@ public void deleteExerciseLessonsQuestion(@PathVariable String lessonsQuestionId
         public void sendExerciseLessons(@PathVariable String exerciseId, @Valid @RequestBody LessonsSendInput input) {
             Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow();
             List<LessonsCategory> lessonsCategories = lessonsCategoryRepository.findAll(LessonsCategorySpecification.fromExercise(exerciseId)).stream().toList();
    -        EmailContent emailContent = new EmailContent();
    -        emailContent.setSubject(input.getSubject());
    -        emailContent.setBody(input.getBody());
    -        Inject inject = new Inject();
    -        inject.setTitle("Lessons learned campaign");
    -        inject.setDescription("Direct inject for lessons learned questionnaire");
    -        inject.setContent(mapper.valueToTree(emailContent));
    -        inject.setContract(EmailContract.EMAIL_DEFAULT);
    -        inject.setUser(currentUser());
    -        inject.setDirect(true);
    -        Contract contract = contractService.resolveContract(inject);
    -        if (contract == null) {
    -            throw new UnsupportedOperationException("Unknown inject contract " + inject.getContract());
    -        }
    -        inject.setType(contract.getConfig().getType());
    -        inject.setExercise(exercise);
    -        List<ExecutionContext> userInjectContexts = lessonsCategories.stream().flatMap(lessonsCategory -> lessonsCategory.getAudiences().stream()
    -                        .flatMap(audience -> audience.getUsers().stream())).distinct()
    -                .map(user -> new ExecutionContext(openExConfig, user, inject, "Direct execution")).toList();
    -        ExecutableInject injection = new ExecutableInject(true, true, inject, contract, List.of(), userInjectContexts);
    -        Injector executor = context.getBean(contract.getConfig().getType(), Injector.class);
    -        executor.executeInjection(injection);
    +        List<User> users = lessonsCategories.stream().flatMap(lessonsCategory -> lessonsCategory.getAudiences().stream()
    +                .flatMap(audience -> audience.getUsers().stream())).distinct().toList();
    +        mailingService.sendEmail(input.getSubject(), input.getBody(), users, Optional.of(exercise));
         }
     
         @GetMapping("/api/exercises/{exerciseId}/lessons_answers")
    
  • openex-api/src/main/java/io/openex/rest/user/form/login/ResetUserInput.java+29 0 added
    @@ -0,0 +1,29 @@
    +package io.openex.rest.user.form.login;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +import static io.openex.config.AppConfig.MANDATORY_MESSAGE;
    +
    +public class ResetUserInput {
    +
    +    @NotBlank(message = MANDATORY_MESSAGE)
    +    private String login;
    +
    +    private String lang;
    +
    +    public String getLogin() {
    +        return login;
    +    }
    +
    +    public void setLogin(String login) {
    +        this.login = login;
    +    }
    +
    +    public String getLang() {
    +        return lang;
    +    }
    +
    +    public void setLang(String lang) {
    +        this.lang = lang;
    +    }
    +}
    
  • openex-api/src/main/java/io/openex/rest/user/form/user/ChangePasswordInput.java+34 0 added
    @@ -0,0 +1,34 @@
    +package io.openex.rest.user.form.user;
    +
    +import com.fasterxml.jackson.annotation.JsonProperty;
    +
    +import javax.validation.constraints.NotBlank;
    +
    +import static io.openex.config.AppConfig.MANDATORY_MESSAGE;
    +
    +public class ChangePasswordInput {
    +
    +    @NotBlank(message = MANDATORY_MESSAGE)
    +    @JsonProperty("password")
    +    private String password;
    +
    +    @NotBlank(message = MANDATORY_MESSAGE)
    +    @JsonProperty("password_validation")
    +    private String passwordValidation;
    +
    +    public String getPassword() {
    +        return password;
    +    }
    +
    +    public void setPassword(String password) {
    +        this.password = password;
    +    }
    +
    +    public String getPasswordValidation() {
    +        return passwordValidation;
    +    }
    +
    +    public void setPasswordValidation(String passwordValidation) {
    +        this.passwordValidation = passwordValidation;
    +    }
    +}
    
  • openex-api/src/main/java/io/openex/rest/user/UserApi.java+66 7 modified
    @@ -5,21 +5,26 @@
     import io.openex.database.repository.OrganizationRepository;
     import io.openex.database.repository.TagRepository;
     import io.openex.database.repository.UserRepository;
    +import io.openex.rest.exception.InputValidationException;
     import io.openex.rest.helper.RestBehavior;
     import io.openex.rest.user.form.login.LoginUserInput;
    +import io.openex.rest.user.form.login.ResetUserInput;
    +import io.openex.rest.user.form.user.ChangePasswordInput;
     import io.openex.rest.user.form.user.CreateUserInput;
    -import io.openex.rest.user.form.user.UpdatePasswordInput;
     import io.openex.rest.user.form.user.UpdateUserInput;
    +import io.openex.service.MailingService;
     import io.openex.service.UserService;
    +import org.apache.commons.collections4.map.PassiveExpiringMap;
    +import org.apache.commons.lang3.RandomStringUtils;
     import org.springframework.beans.factory.annotation.Autowired;
    -import org.springframework.http.ResponseEntity;
     import org.springframework.security.access.AccessDeniedException;
     import org.springframework.web.bind.annotation.*;
     
     import javax.annotation.Resource;
     import javax.annotation.security.RolesAllowed;
     import javax.transaction.Transactional;
     import javax.validation.Valid;
    +import java.util.List;
     import java.util.Optional;
     
     import static io.openex.database.model.User.ROLE_ADMIN;
    @@ -28,14 +33,19 @@
     
     @RestController
     public class UserApi extends RestBehavior {
    -
    +    PassiveExpiringMap<String, String> resetTokenMap = new PassiveExpiringMap<>(1000 * 60 * 10);
         @Resource
         private SessionManager sessionManager;
    -
         private OrganizationRepository organizationRepository;
         private UserRepository userRepository;
         private TagRepository tagRepository;
         private UserService userService;
    +    private MailingService mailingService;
    +
    +    @Autowired
    +    public void setMailingService(MailingService mailingService) {
    +        this.mailingService = mailingService;
    +    }
     
         @Autowired
         public void setTagRepository(TagRepository tagRepository) {
    @@ -58,18 +68,67 @@ public void setUserRepository(UserRepository userRepository) {
         }
     
         @PostMapping("/api/login")
    -    public ResponseEntity<User> login(@Valid @RequestBody LoginUserInput input) {
    +    public User login(@Valid @RequestBody LoginUserInput input) {
             Optional<User> optionalUser = userRepository.findByEmail(input.getLogin());
             if (optionalUser.isPresent()) {
                 User user = optionalUser.get();
                 if (userService.isUserPasswordValid(user, input.getPassword())) {
                     userService.createUserSession(user);
    -                return ResponseEntity.ok().body(user);
    +                return user;
                 }
             }
             throw new AccessDeniedException("Invalid credentials");
         }
     
    +    @PostMapping("/api/reset")
    +    public void passwordReset(@Valid @RequestBody ResetUserInput input) {
    +        Optional<User> optionalUser = userRepository.findByEmail(input.getLogin());
    +        if (optionalUser.isPresent()) {
    +            User user = optionalUser.get();
    +            String resetToken = RandomStringUtils.randomNumeric(8);
    +            String username = user.getName() != null ? user.getName() : user.getEmail();
    +            if ("fr".equals(input.getLang())) {
    +                String subject = resetToken + " est votre code de récupération de compte OpenEx";
    +                String body = "Bonjour " + username + ",</br>" +
    +                        "Nous avons reçu une demande de réinitialisation de votre mot de passe OpenEx.</br>" +
    +                        "Entrez le code de réinitialisation du mot de passe suivant : " + resetToken;
    +                mailingService.sendEmail(subject, body, List.of(user));
    +            } else {
    +                String subject = resetToken + " is your recovery code of your OpenEx account";
    +                String body = "Hi " + username + ",</br>" +
    +                        "A request has been made to reset your OpenEx password.</br>" +
    +                        "Enter the following password recovery code: " + resetToken;
    +                mailingService.sendEmail(subject, body, List.of(user));
    +            }
    +            // Store in memory reset token
    +            resetTokenMap.put(resetToken, user.getId());
    +        }
    +    }
    +
    +    @PostMapping("/api/reset/{token}")
    +    public User changePasswordReset(@PathVariable String token, @Valid @RequestBody ChangePasswordInput input) throws InputValidationException {
    +        String userId = resetTokenMap.get(token);
    +        if (userId != null) {
    +            String password = input.getPassword();
    +            String passwordValidation = input.getPasswordValidation();
    +            if (!passwordValidation.equals(password)) {
    +                throw new InputValidationException("password_validation", "Bad password validation");
    +            }
    +            User changeUser = userRepository.findById(userId).orElseThrow();
    +            changeUser.setPassword(userService.encodeUserPassword(password));
    +            User savedUser = userRepository.save(changeUser);
    +            resetTokenMap.remove(token);
    +            return savedUser;
    +        }
    +        // Bad token or expired token
    +        throw new AccessDeniedException("Invalid credentials");
    +    }
    +
    +    @GetMapping("/api/reset/{token}")
    +    public boolean validatePasswordResetToken(@PathVariable String token) {
    +        return resetTokenMap.get(token) != null;
    +    }
    +
         @RolesAllowed(ROLE_ADMIN)
         @GetMapping("/api/users")
         public Iterable<User> users() {
    @@ -79,7 +138,7 @@ public Iterable<User> users() {
         @RolesAllowed(ROLE_ADMIN)
         @PutMapping("/api/users/{userId}/password")
         public User changePassword(@PathVariable String userId,
    -                               @Valid @RequestBody UpdatePasswordInput input) {
    +                               @Valid @RequestBody ChangePasswordInput input) {
             User user = userRepository.findById(userId).orElseThrow();
             user.setPassword(userService.encodeUserPassword(input.getPassword()));
             return userRepository.save(user);
    
  • openex-api/src/main/java/io/openex/service/MailingService.java+71 0 added
    @@ -0,0 +1,71 @@
    +package io.openex.service;
    +
    +import com.fasterxml.jackson.databind.ObjectMapper;
    +import io.openex.config.OpenExConfig;
    +import io.openex.contract.Contract;
    +import io.openex.database.model.Exercise;
    +import io.openex.database.model.Inject;
    +import io.openex.database.model.User;
    +import io.openex.execution.ExecutableInject;
    +import io.openex.execution.ExecutionContext;
    +import io.openex.execution.Injector;
    +import io.openex.injects.email.EmailContract;
    +import io.openex.injects.email.model.EmailContent;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.context.ApplicationContext;
    +import org.springframework.stereotype.Service;
    +
    +import javax.annotation.Resource;
    +import java.util.List;
    +import java.util.Optional;
    +
    +import static io.openex.helper.UserHelper.currentUser;
    +
    +@Service
    +public class MailingService {
    +
    +    @Resource
    +    protected ObjectMapper mapper;
    +
    +    @Resource
    +    private OpenExConfig openExConfig;
    +
    +    private ContractService contractService;
    +
    +    private ApplicationContext context;
    +
    +    @Autowired
    +    public void setContext(ApplicationContext context) {
    +        this.context = context;
    +    }
    +
    +    @Autowired
    +    public void setContractService(ContractService contractService) {
    +        this.contractService = contractService;
    +    }
    +
    +    public void sendEmail(String subject, String body, List<User> users, Optional<Exercise> exercise) {
    +        EmailContent emailContent = new EmailContent();
    +        emailContent.setSubject(subject);
    +        emailContent.setBody(body);
    +        Inject inject = new Inject();
    +        inject.setContent(mapper.valueToTree(emailContent));
    +        inject.setContract(EmailContract.EMAIL_DEFAULT);
    +        inject.setUser(currentUser());
    +        Contract contract = contractService.resolveContract(inject);
    +        if (contract == null) {
    +            throw new UnsupportedOperationException("Unknown inject contract " + inject.getContract());
    +        }
    +        inject.setType(contract.getConfig().getType());
    +        exercise.ifPresent(inject::setExercise);
    +        List<ExecutionContext> userInjectContexts = users.stream().distinct()
    +                .map(user -> new ExecutionContext(openExConfig, user, inject, "Direct execution")).toList();
    +        ExecutableInject injection = new ExecutableInject(false, true, inject, contract, List.of(), userInjectContexts);
    +        Injector executor = context.getBean(contract.getConfig().getType(), Injector.class);
    +        executor.executeInjection(injection);
    +    }
    +
    +    public void sendEmail(String subject, String body, List<User> users) {
    +        sendEmail(subject, body, users, Optional.empty());
    +    }
    +}
    
  • openex-api/src/main/resources/application.properties+1 0 modified
    @@ -104,6 +104,7 @@ logging.logback.rollingpolicy.max-history=7
     #############
     
     # Mail sending config (Always available, mandatory)
    +openex.default-mailer=no-reply@openex.io
     spring.mail.host=smtp.mail.com
     spring.mail.port=587
     spring.mail.username=<username@mail.com>
    
  • openex-framework/src/main/java/io/openex/config/OpenExConfig.java+11 0 modified
    @@ -36,6 +36,9 @@ public class OpenExConfig {
         @JsonProperty("auth_kerberos_enable")
         private boolean authKerberosEnable;
     
    +    @JsonProperty("default_mailer")
    +    private String defaultMailer;
    +
         @JsonIgnore
         private String cookieName = "openex_token";
     
    @@ -140,4 +143,12 @@ public boolean isAuthKerberosEnable() {
         public void setAuthKerberosEnable(boolean authKerberosEnable) {
             this.authKerberosEnable = authKerberosEnable;
         }
    +
    +    public String getDefaultMailer() {
    +        return defaultMailer;
    +    }
    +
    +    public void setDefaultMailer(String defaultMailer) {
    +        this.defaultMailer = defaultMailer;
    +    }
     }
    
  • openex-framework/src/main/java/io/openex/execution/ExecutionContext.java+8 6 modified
    @@ -33,12 +33,14 @@ private ExecutionContext(User user, Exercise exercise, List<String> audiences) {
     
         public ExecutionContext(OpenExConfig config, User user, Injection injection, List<String> audiences) {
             this(user, injection.getExercise(), audiences);
    -        String exerciseId = injection.getExercise().getId();
    -        String queryParams = "?user=" + user.getId() + "&inject=" + injection.getId();
    -        this.put(PLAYER_URI, config.getBaseUrl() + "/private/" + exerciseId + queryParams);
    -        this.put(CHALLENGES_URI, config.getBaseUrl() + "/challenges/" + exerciseId + queryParams);
    -        this.put(SCOREBOARD_URI, config.getBaseUrl() + "/scoreboard/" + exerciseId + queryParams);
    -        this.put(LESSONS_URI, config.getBaseUrl() + "/lessons/" + exerciseId + queryParams);
    +        if (injection.getExercise() != null) {
    +            String exerciseId = injection.getExercise().getId();
    +            String queryParams = "?user=" + user.getId() + "&inject=" + injection.getId();
    +            this.put(PLAYER_URI, config.getBaseUrl() + "/private/" + exerciseId + queryParams);
    +            this.put(CHALLENGES_URI, config.getBaseUrl() + "/challenges/" + exerciseId + queryParams);
    +            this.put(SCOREBOARD_URI, config.getBaseUrl() + "/scoreboard/" + exerciseId + queryParams);
    +            this.put(LESSONS_URI, config.getBaseUrl() + "/lessons/" + exerciseId + queryParams);
    +        }
         }
     
         public ExecutionContext(User user, Exercise exercise, String audience) {
    
  • openex-front/src/actions/Application.js+23 0 modified
    @@ -20,6 +20,29 @@ export const updateParameters = (data) => (dispatch) => {
       )(dispatch);
     };
     
    +export const askReset = (username, locale) => (dispatch) => {
    +  const data = { login: username, lang: locale };
    +  return postReferential(schema.user, '/api/reset', data)(dispatch);
    +};
    +
    +export const resetPassword = (token, values) => (dispatch) => {
    +  const data = { password: values.password, password_validation: values.password_validation };
    +  const ref = postReferential(schema.user, `/api/reset/${token}`, data)(dispatch);
    +  return ref.then((finalData) => {
    +    if (finalData[FORM_ERROR]) {
    +      return finalData;
    +    }
    +    return dispatch({
    +      type: Constants.IDENTITY_LOGIN_SUCCESS,
    +      payload: finalData,
    +    });
    +  });
    +};
    +
    +export const validateResetToken = (token) => (dispatch) => {
    +  return getReferential(null, `/api/reset/${token}`)(dispatch);
    +};
    +
     export const askToken = (username, password) => (dispatch) => {
       const data = { login: username, password };
       const ref = postReferential(schema.user, '/api/login', data)(dispatch);
    
  • openex-front/src/components/i18n.js+1 0 modified
    @@ -275,6 +275,7 @@ export const useFormatter = () => {
       };
       return {
         t: translate,
    +    locale: intl.locale ?? intl.defaultLocale,
         tPick: (label) => (label ? label[intl.locale] ?? label[intl.defaultLocale] : ''),
         n: formatNumber,
         b: formatBytes,
    
  • openex-front/src/public/components/login/Login.js+9 8 modified
    @@ -15,6 +15,7 @@ import {
     import LoginForm from './LoginForm';
     import inject18n from '../../../components/i18n';
     import { storeHelper } from '../../../actions/Schema';
    +import Reset from './Reset';
     
     const styles = () => ({
       container: {
    @@ -36,13 +37,9 @@ const Login = (props) => {
       const { classes, parameters, t } = props;
       const { auth_openid_enable: isOpenId, auth_local_enable: isLocal } = parameters;
       const { platform_providers: providers } = parameters;
    -  const [dimension, setDimension] = useState({
    -    width: window.innerWidth,
    -    height: window.innerHeight,
    -  });
    -  const updateWindowDimensions = () => {
    -    setDimension({ width: window.innerWidth, height: window.innerHeight });
    -  };
    +  const [reset, setReset] = useState(false);
    +  const [dimension, setDimension] = useState({ width: window.innerWidth, height: window.innerHeight });
    +  const updateWindowDimensions = () => setDimension({ width: window.innerWidth, height: window.innerHeight });
       useEffect(() => {
         window.addEventListener('resize', updateWindowDimensions);
         return () => window.removeEventListener('resize', updateWindowDimensions);
    @@ -62,11 +59,15 @@ const Login = (props) => {
       return (
         <div className={classes.container} style={{ marginTop }}>
           <img src={`/${logo}`} alt="logo" className={classes.logo} />
    -      {isLocal && (
    +      {isLocal && !reset && (
             <Paper variant="outlined">
               <LoginForm onSubmit={onSubmit} />
    +          <div style={{ marginBottom: 10 }}>
    +            <a onClick={() => setReset(true)}>{t('I forgot my password')}</a>
    +          </div>
             </Paper>
           )}
    +      {isLocal && reset && <Reset onCancel={() => setReset(false)}/>}
           {isOpenId
             && (providers ?? []).map((provider) => (
               <div key={provider.provider_name}>
    
  • openex-front/src/public/components/login/Reset.js+121 0 added
    @@ -0,0 +1,121 @@
    +import React, { useState } from 'react';
    +import { useDispatch } from 'react-redux';
    +import Paper from '@mui/material/Paper';
    +import Button from '@mui/material/Button';
    +import { Form } from 'react-final-form';
    +import { makeStyles } from '@mui/styles';
    +import { askReset, resetPassword, validateResetToken } from '../../../actions/Application';
    +import { useFormatter } from '../../../components/i18n';
    +import { TextField } from '../../../components/TextField';
    +
    +const useStyles = makeStyles(() => ({
    +  container: {
    +    textAlign: 'center',
    +    margin: '0 auto',
    +    width: 400,
    +  },
    +  appBar: {
    +    borderTopLeftRadius: '10px',
    +    borderTopRightRadius: '10px',
    +  },
    +  logo: {
    +    width: 200,
    +    margin: '0px 0px 50px 0px',
    +  },
    +}));
    +
    +const validateFields = (t, values, requiredFields) => {
    +  const errors = {};
    +  requiredFields.forEach((field) => {
    +    if (!values[field]) {
    +      errors[field] = t('This field is required.');
    +    }
    +  });
    +  return errors;
    +};
    +
    +const STEP_ASK_RESET = 'ask';
    +const STEP_VALIDATE_TOKEN = 'validate';
    +const STEP_RESET_PASSWORD = 'reset';
    +const Reset = ({ onCancel }) => {
    +  const classes = useStyles();
    +  const { t, locale } = useFormatter();
    +  const dispatch = useDispatch();
    +  const [step, setStep] = useState(STEP_ASK_RESET);
    +  const [token, setToken] = useState();
    +  const onSubmitAskToken = (data) => {
    +    dispatch(askReset(data.username, locale)).then(() => {
    +      setStep(STEP_VALIDATE_TOKEN);
    +    });
    +  };
    +  const onSubmitValidateToken = (data) => {
    +    dispatch(validateResetToken(data.code)).then((response) => {
    +      if (response) {
    +        setToken(data.code);
    +        setStep(STEP_RESET_PASSWORD);
    +      }
    +    });
    +  };
    +  const onSubmitValidatePassword = (data) => dispatch(resetPassword(token, data));
    +  return (
    +    <div className={classes.container}>
    +      <Paper variant="outlined">
    +        <div style={{ padding: 15 }}>
    +          {step === STEP_ASK_RESET && <Form onSubmit={onSubmitAskToken}
    +                                   validate={(values) => validateFields(t, values, ['username'])}>
    +            {({ handleSubmit, submitting, pristine }) => (
    +                <form onSubmit={handleSubmit}>
    +                  <TextField name="username" type="text"
    +                      variant="standard" label={t('Email address')}
    +                      fullWidth={true} style={{ marginTop: 5 }}/>
    +                  <Button type="submit" variant="contained"
    +                      disabled={pristine || submitting} onClick={handleSubmit}
    +                      style={{ marginTop: 30 }}>
    +                    {t('Send reset code')}
    +                  </Button>
    +                </form>
    +            )}
    +          </Form>}
    +          {step === STEP_VALIDATE_TOKEN && <Form onSubmit={onSubmitValidateToken}
    +                                        validate={(values) => validateFields(t, values, ['code'])}>
    +            {({ handleSubmit, submitting, pristine }) => (
    +                <form onSubmit={handleSubmit}>
    +                  <TextField name="code" type="text"
    +                             variant="standard" label={t('Enter code')}
    +                             fullWidth={true} style={{ marginTop: 5 }}/>
    +                  <Button type="submit" variant="contained"
    +                          disabled={pristine || submitting} onClick={handleSubmit}
    +                          style={{ marginTop: 30 }}>
    +                    {t('Continue')}
    +                  </Button>
    +                </form>
    +            )}
    +          </Form>}
    +          {step === STEP_RESET_PASSWORD && <Form onSubmit={onSubmitValidatePassword}
    +                                                 validate={(values) => validateFields(t, values, ['password', 'password_validation'])}>
    +            {({ handleSubmit, submitting, pristine }) => (
    +                <form onSubmit={handleSubmit}>
    +                  <TextField name="password" type="password"
    +                             variant="standard" label={t('Password')}
    +                             fullWidth={true} style={{ marginTop: 5 }}/>
    +                  <TextField name="password_validation" type="password"
    +                             variant="standard" label={t('Password validation')}
    +                             fullWidth={true} style={{ marginTop: 5 }}/>
    +                  <Button type="submit" variant="contained"
    +                          disabled={pristine || submitting} onClick={handleSubmit}
    +                          style={{ marginTop: 30 }}>
    +                    {t('Change your password')}
    +                  </Button>
    +                </form>
    +            )}
    +          </Form>}
    +          <div style={{ marginTop: 10 }}>
    +            <a onClick={() => onCancel()}>{t('Back to login')}</a>
    +          </div>
    +        </div>
    +      </Paper>
    +    </div>
    +  );
    +};
    +
    +export default Reset;
    
  • openex-front/src/public/Index.js+5 13 modified
    @@ -8,6 +8,7 @@ import { errorWrapper } from '../components/Error';
     import Media from './components/medias/Media';
     import Challenges from './components/challenges/Challenges';
     import Lessons from './components/lessons/Lessons';
    +import Reset from './components/login/Reset';
     
     const useStyles = makeStyles((theme) => ({
       root: {
    @@ -37,19 +38,10 @@ const Index = () => {
         <div className={classes.root}>
           <main className={classes.content}>
             <Switch>
    -          <Route
    -            exact
    -            path="/comcheck/:statusId"
    -            render={errorWrapper(Comcheck)}
    -          />
    -          <Route
    -            path="/medias/:exerciseId/:mediaId"
    -            render={errorWrapper(Media)}
    -          />
    -          <Route
    -            path="/challenges/:exerciseId"
    -            render={errorWrapper(Challenges)}
    -          />
    +          <Route exact path="/comcheck/:statusId" render={errorWrapper(Comcheck)}/>
    +          <Route exact path="/reset" render={errorWrapper(Reset)}/>
    +          <Route path="/medias/:exerciseId/:mediaId" render={errorWrapper(Media)}/>
    +          <Route path="/challenges/:exerciseId" render={errorWrapper(Challenges)}/>
               <Route path="/lessons/:exerciseId" render={errorWrapper(Lessons)} />
               <Route component={Login} />
             </Switch>
    
  • openex-front/src/resources/css/main.css+1 0 modified
    @@ -14,6 +14,7 @@ a,
     a:hover,
     a:visited,
     a:focus {
    +  cursor: pointer;
       text-decoration: none;
     }
     
    
  • openex-front/src/utils/Localization.js+7 0 modified
    @@ -6,7 +6,14 @@ const i18n = {
             'OpenEx - Plateforme d’exercices de crise',
           'Email address': 'Adresse email',
           Password: 'Mot de passe',
    +      'Password validation': 'Validation du mot de passe',
    +      'Change your password': 'Changer votre mot de passe',
    +      'I forgot my password': 'J\'ai oublié mon mot de passe',
    +      'Send reset code': 'Envoyer le code',
    +      'Enter code': 'Entrer le code',
    +      'Back to login': 'Retour à l\'identification',
           'Sign in': "S'identifier",
    +      Continue: 'Continuer',
           Dashboard: 'Tableau de bord',
           Exercises: 'Exercices',
           Players: 'Joueurs',
    
  • openex-model/pom.xml+6 0 modified
    @@ -34,6 +34,12 @@
                 <artifactId>spring-boot-starter-security</artifactId>
                 <version>${spring.version}</version>
             </dependency>
    +        <dependency>
    +            <groupId>org.springframework.boot</groupId>
    +            <artifactId>spring-boot-configuration-processor</artifactId>
    +            <version>${spring.version}</version>
    +            <optional>true</optional>
    +        </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-oauth2-client</artifactId>
    
  • openex-model/src/main/java/io/openex/database/model/Exercise.java+1 1 modified
    @@ -77,7 +77,7 @@ public enum STATUS {
     
         @Column(name = "exercise_mail_from")
         @JsonProperty("exercise_mail_from")
    -    private String replyTo = "planners@openex.io";
    +    private String replyTo;
     
         @ManyToOne(fetch = FetchType.LAZY)
         @JoinColumn(name = "exercise_logo_dark")
    
  • openex-model/src/main/java/io/openex/database/model/Inject.java+0 11 modified
    @@ -147,9 +147,6 @@ public class Inject implements Base, Injection {
         private List<InjectExpectation> expectations = new ArrayList<>();
     
         // region transient
    -    @Transient
    -    private boolean direct = false;
    -
         @Transient
         public String getHeader() {
             return ofNullable(getExercise()).map(Exercise::getHeader).orElse("");
    @@ -248,14 +245,6 @@ public void setEnabled(boolean enabled) {
             this.enabled = enabled;
         }
     
    -    public boolean isDirect() {
    -        return direct;
    -    }
    -
    -    public void setDirect(boolean direct) {
    -        this.direct = direct;
    -    }
    -
         public Instant getCreatedAt() {
             return createdAt;
         }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.