{"record":{"id":"5787edf2eebe049e","repo":"signalapp/Signal-Server","slug":"end-of-range-too-far-in-the-future","errorCode":null,"errorMessage":"end of range too far in the future","messagePattern":"end of range too far in the future","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java","lineNumber":68,"sourceCode":"      throws IllegalArgumentException {\n    final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\n    final Instant yesterday = today.minus(Duration.ofDays(1));\n\n    if (redemptionStart.isAfter(redemptionEnd)) {\n      throw new IllegalArgumentException(\"end of range must be after start of range\");\n    }\n\n    if (!redemptionStart.truncatedTo(ChronoUnit.DAYS).equals(redemptionStart)\n        || !redemptionEnd.truncatedTo(ChronoUnit.DAYS).equals(redemptionEnd)) {\n      throw new IllegalArgumentException(\"timestamps must be day aligned\");\n    }\n\n    if (redemptionStart.isBefore(yesterday)) {\n      throw new IllegalArgumentException(\"start of range too far in the past\");\n    }\n\n    if (redemptionEnd.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)))) {\n      throw new IllegalArgumentException(\"end of range too far in the future\");\n    }\n\n    if (redemptionEnd.isAfter(redemptionStart.plus(MAX_REDEMPTION_DURATION))) {\n      throw new IllegalArgumentException(\"redemption window too large\");\n    }\n\n    return new RedemptionRange(\n        LocalDate.ofInstant(redemptionStart, ZoneOffset.UTC),\n        LocalDate.ofInstant(redemptionEnd, ZoneOffset.UTC));\n  }\n\n  @Override\n  public @NotNull Iterator<Instant> iterator() {\n    final Instant fromInstant = from.atStartOfDay(ZoneOffset.UTC).toInstant();\n    final Instant endInstant = end.atStartOfDay(ZoneOffset.UTC).toInstant();\n    return Stream\n        .iterate(fromInstant, redemptionTime -> redemptionTime.plus(Duration.ofDays(1)))\n        .takeWhile(redemptionTime -> !redemptionTime.isAfter(endInstant))","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/RedemptionRange.java#L50-L86","documentation":"RedemptionRange.inclusive() rejects ranges whose end extends beyond today plus MAX_REDEMPTION_DURATION plus one day. This prevents callers from requesting redemption windows reaching into the far future.","triggerScenarios":"Calling inclusive() with redemptionEnd > truncatedToday + MAX_REDEMPTION_DURATION + 1 day, e.g. pre-scheduling a redemption range months ahead.","commonSituations":"Generating long-lived default windows; client sending an end date far ahead; misinterpreting the allowed maximum window length constant.","solutions":["Clamp redemptionEnd to today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)).","Compute the end as start.plus(MAX_REDEMPTION_DURATION) capped by the future limit.","Surface the allowed maximum to clients so they request valid windows."],"exampleFix":"// before\nInstant end = start.plus(Duration.ofDays(365));\nRedemptionRange.inclusive(clock, start, end);\n// after\nInstant end = start.plus(MAX_REDEMPTION_DURATION);\nRedemptionRange.inclusive(clock, start, end);","handlingStrategy":"validation","validationCode":"Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\nInstant maxEnd = today.plus(Duration.ofDays(MAX_WINDOW_DAYS)).plus(Duration.ofDays(1));\nif (end.isAfter(maxEnd)) { end = maxEnd; }","typeGuard":"boolean isEndWithinPolicy(Instant end, Clock clock) {\n  Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);\n  return !end.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)));\n}","tryCatchPattern":"try {\n  RedemptionRange.inclusive(clock, start, end);\n} catch (IllegalArgumentException e) {\n  if (e.getMessage().contains(\"too far in the future\")) {\n    end = clampEnd(clock);\n    return RedemptionRange.inclusive(clock, start, end);\n  }\n  throw e;\n}","preventionTips":["Derive end dates from start plus a bounded duration instead of accepting arbitrary client input","Expose the maximum allowed end date in API documentation","Add request-level validation returning 400 before reaching the domain check"],"tags":["validation","date-range","bounds-check"],"backgroundTag":"value-out-of-range","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}