Skip to content

3.6.0 — Security Maintenance (Spring Boot 3.5 / Java 17)

Choose a tag to compare

@devondragon devondragon released this 16 Jun 01:46
· 404 commits to main since this release

Maven Central: com.digitalsanctuary:ds-spring-user-framework:3.6.0


Security-maintenance release for the Spring Boot 3.5 / Java 17 line. This backports the security-critical
fixes from the 4.4.0 and 5.0.x (Spring Boot 4 / Java 21) releases that apply to code present in the 3.5.x line.
It deliberately does not include the larger architectural changes, new features (WebAuthn, MFA, GDPR service,
token-at-rest hashing), or breaking HTTP-contract changes from those releases. The 3.x line is now in
security-maintenance only
— new development happens on the 5.0.x line (Spring Boot 4.0/4.1, Java 21). Java-21
users should migrate to 5.0.x.

Features

  • None

Fixes

  • Security: Optional canonical app URL to mitigate host-header poisoning (CWE-640)
    • Added new configuration property user.security.appUrl. When set (e.g. https://app.example.com), password-reset and email-verification links are built from this value and the X-Forwarded-Host header is ignored.
    • Implementation details:
      • UserAPI now injects user.security.appUrl and uses a new resolveAppUrl(HttpServletRequest) helper.
      • Trailing slash is stripped; if the property is blank, behavior falls back to the legacy UserUtils.getAppUrl(request).
      • All link creation paths now call resolveAppUrl: registration verification email, forgot-password email, and registration event publishing.
      • Spring configuration metadata updated to document the new property.
    • Default remains unchanged (blank), preserving prior behavior. Recommended to set this property when running behind a reverse proxy to close the poisoning vector.
  • Security: Prevent audit log forging/corruption via CR/LF and delimiter injection
    • FileAuditLogWriter now sanitizes all attacker-influenceable fields (action, status, userId, email, IP, sessionId, message, userAgent, extraData) by replacing CR, LF, and | with spaces before writing the pipe-delimited record.
    • Ensures each record remains exactly one line with 10 fields, preventing newline-based record forging and delimiter-based column shifting.
  • Security: Reject OAuth2/OIDC logins where providers explicitly report unverified emails
    • Google (OAuth2 userinfo): getUserFromGoogleOAuth2User now rejects when email_verified is explicitly false (Boolean or case-insensitive String "false"). Absent claim remains trusted.
    • OIDC providers: rejects when standard email_verified claim is explicitly false.
    • Facebook: getUserFromFacebookOAuth2User now rejects when either email_verified (newer Graph API) or verified (older) claims are explicitly false. Absent claims remain trusted.
    • All three paths throw an OAuth2AuthenticationException with error code email_not_verified and log a warning.
  • Security: Sanitize OAuth2 failure messaging to avoid sensitive data leakage
    • Introduced SanitizingOAuth2AuthenticationFailureHandler; on authentication failure it:
      • Logs full exception details server-side.
      • Stores only a generic, user-safe message in session attribute error.message, or a specific non-sensitive message when the error is email_not_verified.
      • Redirects back to the configured login page.
    • WebSecurityConfig now wires this handler into oauth2Login(). This prevents raw exception messages (which may include account emails or provider details) from reaching the browser.
  • Concurrency: Atomic failed-login increment to close lockout-evasion race
    • New repository method UserRepository.incrementFailedAttempts(email) performs a single JPQL UPDATE u.failedLoginAttempts = u.failedLoginAttempts + 1 where u.email = :email with @Modifying(clearAutomatically = true, flushAutomatically = true).
    • LoginAttemptService.loginFailed now:
      • Uses the atomic increment to avoid lost updates under parallel failures.
      • Re-reads the user, and if the threshold is reached, locks and timestamps the account (idempotent lock).
    • Prevents attackers from evading lockout by generating concurrent failures that previously could lose increments.
  • Security: Reduce secret leakage in logs
    • User.toString() now excludes the password field via @ToString.Exclude (prevents bcrypt hash from appearing in logs).
    • Logs no longer include raw tokens or full principals:
      • Sensitive tokens are logged via a short fingerprint (first 6 chars + ellipsis; short tokens masked as ****).
      • Authentication/principal logs report only usernames/emails or principal names; attribute dumps replaced with attribute key sets.
    • Affects logging in UserActionController, UserVerificationService, DSOAuth2UserService, DSOidcUserService, LoginSuccessService, LogoutSuccessService, UserEmailService, UserService, and JpaAuditingConfig.

Breaking Changes

  • OAuth2 login failure UI messaging is now generic
    • Previously, raw AuthenticationException messages were stored in the session and could be displayed by the UI. Now only a generic error or the non-sensitive “email not verified” message is stored.
    • If your UI or integration logic depended on specific raw exception text, update it to rely on generic messaging or server-side logs for detail.

Refactoring

  • Internal cleanup in LoginAttemptService:
    • Removed the old read-modify-write incrementFailedAttempts(User) method in favor of the repository-level atomic increment.
  • Logging refactors to reduce sensitive output (see Fixes).

Documentation

  • CHANGELOG.md: New 3.6.0 section summarizing backported security fixes and maintenance-only posture for the Spring Boot 3.5 / Java 17 line.
  • README.md:
    • Updated installation coordinates to version 3.6.0.
    • Added a maintenance-only banner directing Java 21/Spring Boot 4 users to the 5.0.x line.
  • CONFIG.md:
    • Documented the new user.security.appUrl property, including guidance on when to enable it and its security implications.
  • gradle.properties: Bumped to 3.6.0-SNAPSHOT for development (release plugin produces 3.6.0).

Testing

  • UserAPIUnitTest: Added “App URL resolution (CWE-640)” tests:
    • Verifies configured appUrl overrides X-Forwarded-Host, trailing slash stripping, and fallback to request-derived URL when blank.
  • UserRepositoryTest: New repository slice tests for incrementFailedAttempts:
    • Confirms row count returns and atomic increments via two successive updates; verifies zero rows for non-existent emails.
  • LoginAttemptServiceTest: Updated and expanded:
    • Asserts atomic increment usage, correct locking at threshold, no lock below threshold, behavior when user missing, and when lockout is disabled.
  • SanitizingOAuth2AuthenticationFailureHandlerTest: Ensures generic/non-leaking messages are stored in session and redirects occur, including the specific mapping for email_not_verified.
  • DSOAuth2UserServiceTest and DSOidcUserServiceTest:
    • Added comprehensive tests covering acceptance (true/absent) and rejection (explicit false) of email_verified claims for Google and OIDC providers.
  • UserToStringTest: Verifies that User.toString() does not include the password hash.

Other Changes

  • Build: Gradle release configuration widened to allow release/* branches
    • build.gradle now sets net.researchgate.release git.requireBranch to main|release/.* so security-maintenance releases off release/* (e.g., release/3.6.0) can run ./gradlew release.
    • Still accepts exactly main.