Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
val path = type.directory + "/" + hashedFilename
val fullPath = rootLocation.resolve(path).normalize()

log.info("Storing {} at {}", multipart.originalFilename, fullPath)
val safeFilename = (multipart.originalFilename ?: "<null>").replace(Regex("\\p{Cntrl}"), "_")
log.info("Storing {} at {}", safeFilename, fullPath)
Comment thread
Copilot marked this conversation as resolved.

if (Files.exists(fullPath)) {
Files.deleteIfExists(tmp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@
handler: Any?,
ex: Exception,
): ModelAndView? {
// Strip CR/LF from the request URI and exception message before logging (log-injection, #464).
val safeUri = request.requestURI.replace('\r', '_').replace('\n', '_')
val safeMessage = (ex.message ?: "<null>").replace('\r', '_').replace('\n', '_')
log.error(
"{} {} -> {}: {}",
request.method,
request.requestURI,
safeUri,
ex.javaClass.simpleName,
ex.message,
safeMessage,
ex,
)

Check warning

Code scanning / CodeQL

Log Injection Medium

This log entry depends on a
user-provided value
.
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,27 @@ class BrevoListAdapter(
override val system = ContactSystem.BREVO

override fun createList(name: String, folderName: String?): Long {
log.info("Creating Brevo list '{}'", name)
val safeName = sanitizeForLog(name)
log.info("Creating Brevo list '{}'", safeName)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return try {
val req = CreateListRequest()
req.name = name
req.folderId = contributionPeriodsFolder
val response = contactsApi.createList(req)
log.info("Created Brevo list '{}' id={}", name, response.id)
log.info("Created Brevo list '{}' id={}", safeName, response.id)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
response.id
} catch (e: RestClientResponseException) {
log.error("Failed to create Brevo list '{}'", name, e)
log.error("Failed to create Brevo list '{}'", safeName, e)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
throw ContactServiceException("Failed to create list", e)
}
}

private fun sanitizeForLog(value: String): String = buildString(value.length) {
value.forEach { ch ->
append(if (ch.isISOControl()) '_' else ch)
}
}

override fun addToList(externalUserId: Long, externalListId: Long) {
log.info("Adding Brevo contact {} to list {}", externalUserId, externalListId)
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ class EmailTrackingController(
EmailDeliveryStatus.DELIVERED -> emailService.markOpened(outbox)
else -> { /* already opened, bounced, or failed — no state change */ }
}
log.debug("Tracking pixel fired for outbox id={} token={}", outbox.id, token)
log.debug("Tracking pixel fired for outbox id={}", outbox.id)
} else {
log.warn("Tracking pixel fired for unknown token={}", token)
log.warn("Tracking pixel fired for unknown token (not found in outbox)")
}
}.onFailure { log.warn("Error recording email open for token={}", token, it) }
}.onFailure { log.warn("Error recording email open", it) }

val headers = HttpHeaders()
headers.contentType = MediaType.IMAGE_GIF
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class MockContactAdapter : ContactAdapter, ContactListAdapter {
private val contactIdSequence = AtomicLong(1000)
private val listIdSequence = AtomicLong(2000)

private fun sanitizeForLog(value: String): String =
value.map { ch -> if (ch == '\r' || ch == '\n' || ch.isISOControl()) '_' else ch }.joinToString("")

// ── ContactSyncAdapter ────────────────────────────────────────────────────

override fun createContact(data: ContactData): Long {
Expand All @@ -49,7 +52,8 @@ class MockContactAdapter : ContactAdapter, ContactListAdapter {
isMember = data.isMember,
attributes = data.attributes.toMutableMap()
)
log.info("Mock: Created contact id={} for {}", contactId, data.email)
val safeEmail = sanitizeForLog(data.email)
log.info("Mock: Created contact id={} for {}", contactId, safeEmail)
return contactId
}

Expand All @@ -73,15 +77,17 @@ class MockContactAdapter : ContactAdapter, ContactListAdapter {
val removed = contacts.remove(externalId)
?: throw ContactServiceException("Mock: Contact not found: $externalId")
memberships.keys.removeIf { (contactId, _) -> contactId == externalId }
log.info("Mock: Deleted contact id={} ({})", externalId, removed.email)
val safeEmail = sanitizeForLog(removed.email)
log.info("Mock: Deleted contact id={} ({})", externalId, safeEmail)
}

// ── ListSyncAdapter ───────────────────────────────────────────────────────

override fun createList(name: String, folderName: String?): Long {
val listId = listIdSequence.getAndIncrement()
lists[listId] = MockList(listId = listId, listName = name, folderName = folderName)
log.info("Mock: Created list id={} name='{}'", listId, name)
val safeName = sanitizeForLog(name)
log.info("Mock: Created list id={} name='{}'", listId, safeName)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return listId
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@

private val log = LoggerFactory.getLogger(javaClass)

private fun sanitizeForLog(value: String): String =
value.replace(Regex("[\\r\\n\\t\\u0000-\\u001F\\u007F]"), " ")
private fun sanitizeForLog(value: String?): String =
value.orEmpty().replace(Regex("\\p{Cntrl}"), "_")

@GetMapping
@PermitAll
Expand All @@ -58,6 +58,8 @@
request: HttpServletRequest,
): ResponseEntity<Void> {
val forwardedHost = request.getHeader("X-Forwarded-Host").orEmpty()
// CR/LF-stripped copy of the attacker-controllable host header for logging (log-injection, #464).
val safeHost = sanitizeForLog(forwardedHost)
val forwardedUri = request.getHeader("X-Forwarded-Uri").orEmpty().ifEmpty { "/" }
val forwardedProto = request.getHeader("X-Forwarded-Proto").orEmpty().ifEmpty { "https" }
val originalUrl = "$forwardedProto://$forwardedHost$forwardedUri"
Expand All @@ -66,7 +68,7 @@
// Fail-closed: an unknown host (mis-configured IngressRoute, or
// someone pointing forward-auth at us via Host injection) gets
// ADMIN-required. Warn so the operator notices.
log.warn("forward-auth: unknown host '{}' — defaulting to ADMIN", sanitizeForLog(forwardedHost))
log.warn("forward-auth: unknown host '{}' — defaulting to ADMIN", safeHost)
Comment thread
Copilot marked this conversation as resolved.
Role.ADMIN
}

Expand All @@ -87,7 +89,7 @@
} else {
log.warn(
"forward-auth: rejecting redirect to untrusted host '{}' — omitting redirect param",
sanitizeForLog(forwardedHost),
safeHost,
Comment thread
Copilot marked this conversation as resolved.
)
""
}
Expand Down
Loading