forked from vivo-project/Vitro
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix/captcha repeater vulnerability (vivo-project#427)
* Added captcha generation using nano captcha library. Added initial validation logic. * Added variable challenge length. * Added Google reCaptcha. Added simple configuration feature toggle. * Added java docs. * Fixed log4J version bug. Added unit tests for captcha functionality. * Added guava cache for mitigation of DoS by generating challenges. * Aligned sl4j-api dependency version. * Added refresh button for default challenge. * Improved error messages and added localization. * Update home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: Benjamin Kampe <[email protected]> * Update home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: salmjunior <[email protected]> * Update home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: salmjunior <[email protected]> * Added missing licence header. Refactored code to avoid passing vreq where unnecessary. * Update home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: Georgy Litvinov <[email protected]> * Update home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: Georgy Litvinov <[email protected]> * Update home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: Georgy Litvinov <[email protected]> * Update home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl Co-authored-by: Georgy Litvinov <[email protected]> * Switched to using getInstance() instead of deprecated getBean() method. * Fixed frontend display error. * Added captcha feature toggle with difficulty setting. * Updated captcha configuration. * Made nanocaptcha challenge little bigger. * Improved spanish localization. * Made configuration options as enumertions. * Made configuration case insensitive, updated configuration docs. * Improved french localization. * Refactored code to use provider pattern. * Updated javadocs. * Update home/src/main/resources/config/example.runtime.properties Co-authored-by: Georgy Litvinov <[email protected]> --------- Co-authored-by: Benjamin Kampe <[email protected]> Co-authored-by: salmjunior <[email protected]> Co-authored-by: Georgy Litvinov <[email protected]>
- Loading branch information
1 parent
440d4e8
commit 2df1164
Showing
27 changed files
with
1,619 additions
and
254 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/AbstractCaptchaProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
|
||
import org.apache.commons.logging.Log; | ||
import org.apache.commons.logging.LogFactory; | ||
|
||
/** | ||
* The AbstractCaptchaProvider is an abstract class providing a base structure for captcha providers. | ||
* It includes methods for generating refresh challenges, adding captcha-related fields to page context, | ||
* and validating user inputs against captcha challenges. | ||
* | ||
* @see CaptchaBundle | ||
*/ | ||
public abstract class AbstractCaptchaProvider { | ||
|
||
protected static final Log log = LogFactory.getLog(AbstractCaptchaProvider.class.getName()); | ||
|
||
/** | ||
* Generates a refresh challenge, typically used for updating the captcha displayed on the page. | ||
* Returns empty CaptchaBundle in case of 3rd party implementations | ||
* | ||
* @return CaptchaBundle containing the refreshed captcha challenge. | ||
* @throws IOException If there is an issue generating the refresh challenge. | ||
*/ | ||
abstract CaptchaBundle generateRefreshChallenge() throws IOException; | ||
|
||
/** | ||
* Adds captcha-related fields to the provided page context, allowing integration with web pages. | ||
* | ||
* @param context The context map representing the page's variables. | ||
* @throws IOException If there is an issue adding captcha-related fields to the page context. | ||
*/ | ||
abstract void addCaptchaRelatedFieldsToPageContext(Map<String, Object> context) throws IOException; | ||
|
||
/** | ||
* Validates the user input against a captcha challenge identified by the provided challengeId. | ||
* | ||
* @param captchaInput The user's input to be validated. | ||
* @param challengeId The identifier of the captcha challenge (ignored in case of 3rd party implementations). | ||
* @return True if the input is valid, false otherwise. | ||
*/ | ||
boolean validateCaptcha(String captchaInput, String challengeId) { | ||
return false; | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* $This file is distributed under the terms of the license in LICENSE$ */ | ||
|
||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
import java.util.Objects; | ||
|
||
/** | ||
* Represents a bundle containing a CAPTCHA image in Base64 format, the associated code, | ||
* and a unique challenge identifier. | ||
* | ||
* @author Ivan Mrsulja | ||
* @version 1.0 | ||
*/ | ||
public class CaptchaBundle { | ||
|
||
private final String b64Image; | ||
|
||
private final String code; | ||
|
||
private final String challengeId; | ||
|
||
|
||
public CaptchaBundle(String b64Image, String code, String challengeId) { | ||
this.b64Image = b64Image; | ||
this.code = code; | ||
this.challengeId = challengeId; | ||
} | ||
|
||
public String getB64Image() { | ||
return b64Image; | ||
} | ||
|
||
public String getCode() { | ||
return code; | ||
} | ||
|
||
public String getCaptchaId() { | ||
return challengeId; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) { | ||
return true; | ||
} | ||
if (o == null || getClass() != o.getClass()) { | ||
return false; | ||
} | ||
CaptchaBundle that = (CaptchaBundle) o; | ||
return Objects.equals(code, that.code) && Objects.equals(challengeId, that.challengeId); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(code, challengeId); | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaDifficulty.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
public enum CaptchaDifficulty { | ||
EASY, | ||
HARD | ||
} |
7 changes: 7 additions & 0 deletions
7
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaImplementation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
public enum CaptchaImplementation { | ||
RECAPTCHAV2, | ||
NANOCAPTCHA, | ||
NONE; | ||
} |
138 changes: 138 additions & 0 deletions
138
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
/* $This file is distributed under the terms of the license in LICENSE$ */ | ||
|
||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
import java.io.IOException; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import com.google.common.base.Strings; | ||
import com.google.common.cache.Cache; | ||
import com.google.common.cache.CacheBuilder; | ||
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; | ||
|
||
|
||
/** | ||
* This class provides services related to CAPTCHA challenges and validation. | ||
* It includes method delegates for generating challenges, validating challenge responses, | ||
* and managing CAPTCHA challenges for specific hosts. | ||
* | ||
* @author Ivan Mrsulja | ||
* @version 1.0 | ||
*/ | ||
public class CaptchaServiceBean { | ||
|
||
private static final Cache<String, CaptchaBundle> captchaChallenges = | ||
CacheBuilder.newBuilder() | ||
.maximumSize(1000) | ||
.expireAfterWrite(5, TimeUnit.MINUTES) | ||
.build(); | ||
|
||
private static AbstractCaptchaProvider captchaProvider; | ||
|
||
static { | ||
CaptchaImplementation captchaImplementation = getCaptchaImpl(); | ||
switch (captchaImplementation) { | ||
case RECAPTCHAV2: | ||
captchaProvider = new Recaptchav2Provider(); | ||
break; | ||
case NANOCAPTCHA: | ||
captchaProvider = new NanocaptchaProvider(); | ||
break; | ||
case NONE: | ||
captchaProvider = new DummyCaptchaProvider(); | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* Generates a new CAPTCHA challenge (returns empty CaptchaBundle for 3rd party providers). | ||
* | ||
* @return A CaptchaBundle containing the CAPTCHA image in Base64 format, the content, | ||
* and a unique identifier. | ||
* @throws IOException If an error occurs during image conversion. | ||
*/ | ||
public static CaptchaBundle generateRefreshedChallenge() throws IOException { | ||
return captchaProvider.generateRefreshChallenge(); | ||
} | ||
|
||
/** | ||
* Retrieves a CAPTCHA challenge for a specific host based on the provided CAPTCHA ID | ||
* Removes the challenge from the storage after retrieval. | ||
* | ||
* @param captchaId The CAPTCHA ID to match. | ||
* @return An Optional containing the CaptchaBundle if a matching challenge is found, | ||
* or an empty Optional otherwise. | ||
*/ | ||
public static Optional<CaptchaBundle> getChallenge(String captchaId) { | ||
CaptchaBundle challengeForHost = captchaChallenges.getIfPresent(captchaId); | ||
if (challengeForHost == null) { | ||
return Optional.empty(); | ||
} | ||
|
||
captchaChallenges.invalidate(captchaId); | ||
|
||
return Optional.of(challengeForHost); | ||
} | ||
|
||
/** | ||
* Gets the map containing CAPTCHA challenges for different hosts. | ||
* | ||
* @return A ConcurrentHashMap with host addresses as keys and CaptchaBundle objects as values. | ||
*/ | ||
public static Cache<String, CaptchaBundle> getCaptchaChallenges() { | ||
return captchaChallenges; | ||
} | ||
|
||
/** | ||
* Retrieves the configured captcha implementation based on the application's configuration properties. | ||
* If captcha functionality is disabled, returns NONE. If the captcha implementation is not specified, | ||
* defaults to NANOCAPTCHA. | ||
* | ||
* @return The selected captcha implementation (NANOCAPTCHA, RECAPTCHAv2, or NONE). | ||
*/ | ||
public static CaptchaImplementation getCaptchaImpl() { | ||
String captchaEnabledSetting = ConfigurationProperties.getInstance().getProperty("captcha.enabled"); | ||
|
||
if (Objects.nonNull(captchaEnabledSetting) && !Boolean.parseBoolean(captchaEnabledSetting)) { | ||
return CaptchaImplementation.NONE; | ||
} | ||
|
||
String captchaImplSetting = | ||
ConfigurationProperties.getInstance().getProperty("captcha.implementation"); | ||
|
||
if (Strings.isNullOrEmpty(captchaImplSetting) || | ||
(!captchaImplSetting.equalsIgnoreCase(CaptchaImplementation.RECAPTCHAV2.name()) && | ||
!captchaImplSetting.equalsIgnoreCase(CaptchaImplementation.NANOCAPTCHA.name()))) { | ||
captchaImplSetting = CaptchaImplementation.NANOCAPTCHA.name(); | ||
} | ||
|
||
return CaptchaImplementation.valueOf(captchaImplSetting.toUpperCase()); | ||
} | ||
|
||
/** | ||
* Adds captcha-related fields to the given page context map. The specific fields added depend on the | ||
* configured captcha implementation. | ||
* | ||
* @param context The page context map to which captcha-related fields are added. | ||
* @throws IOException If there is an IO error during captcha challenge generation. | ||
*/ | ||
public static void addCaptchaRelatedFieldsToPageContext(Map<String, Object> context) throws IOException { | ||
CaptchaImplementation captchaImpl = getCaptchaImpl(); | ||
context.put("captchaToUse", captchaImpl.name()); | ||
captchaProvider.addCaptchaRelatedFieldsToPageContext(context); | ||
} | ||
|
||
/** | ||
* Validates a user's captcha input. | ||
* | ||
* @param captchaInput The user's input for the captcha challenge. | ||
* @param challengeId The unique identifier for the challenge (if captcha is 3rd party, this param is ignored). | ||
* @return {@code true} if the captcha input is valid, {@code false} otherwise. | ||
*/ | ||
public static boolean validateCaptcha(String captchaInput, String challengeId) { | ||
return captchaProvider.validateCaptcha(captchaInput, challengeId); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/DummyCaptchaProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package edu.cornell.mannlib.vitro.webapp.beans; | ||
|
||
import java.util.Map; | ||
|
||
/** | ||
* DummyCaptchaProvider is a concrete implementation of AbstractCaptchaProvider, | ||
* serving as a fallback when CAPTCHA is disabled. Validation will always pass, | ||
* in order to fulfill validation logic. | ||
* | ||
* @see AbstractCaptchaProvider | ||
* @see CaptchaBundle | ||
*/ | ||
public class DummyCaptchaProvider extends AbstractCaptchaProvider { | ||
|
||
@Override | ||
CaptchaBundle generateRefreshChallenge() { | ||
return new CaptchaBundle("", "", ""); // No refresh challenges if there is no implementation | ||
} | ||
|
||
@Override | ||
void addCaptchaRelatedFieldsToPageContext(Map<String, Object> context) { | ||
// No added fields necessary if there is no implementation | ||
} | ||
|
||
@Override | ||
boolean validateCaptcha(String captchaInput, String challengeId) { | ||
return true; // validation always passes | ||
} | ||
} |
Oops, something went wrong.