Check if the contents of comment and reply are valid before persistence (#7677)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.21.x

#### What this PR does / why we need it:

This PR checks if the contents of comment and reply are valid before persistence to prevent users from XSS attacks.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/7675

#### Special notes for your reviewer:

Try to comment or reply with the contents from <https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html>.

#### Does this PR introduce a user-facing change?

```release-note
检测评论和回复内容是否合法以防止 XSS 攻击
```
pull/7681/head
John Niang 2025-08-12 12:08:46 +08:00 committed by GitHub
parent 59030f839a
commit 535fe01624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 0 deletions

View File

@ -2,6 +2,9 @@ package run.halo.app.content.comment;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -21,6 +24,7 @@ public abstract class AbstractCommentService {
protected final ReactiveExtensionClient client; protected final ReactiveExtensionClient client;
protected final UserService userService; protected final UserService userService;
protected final CounterService counterService; protected final CounterService counterService;
private final Safelist safelist = Safelist.relaxed();
protected Mono<User> fetchCurrentUser() { protected Mono<User> fetchCurrentUser() {
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
@ -74,4 +78,14 @@ public abstract class AbstractCommentService {
) )
.switchIfEmpty(Mono.fromSupplier(CommentStats::empty)); .switchIfEmpty(Mono.fromSupplier(CommentStats::empty));
} }
/**
* Check if the given html is a safe HTML.
*
* @param html html content
* @return true if the html is safe, false otherwise
*/
protected boolean isSafeHtml(@NonNull String html) {
return Jsoup.isValid(html, safelist);
}
} }

View File

@ -13,6 +13,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.retry.Retry; import reactor.util.retry.Retry;
@ -68,6 +69,13 @@ public class CommentServiceImpl extends AbstractCommentService implements Commen
@Override @Override
public Mono<Comment> create(Comment comment) { public Mono<Comment> create(Comment comment) {
if (comment.getSpec() == null
|| comment.getSpec().getContent() == null
|| !isSafeHtml(comment.getSpec().getContent())) {
return Mono.error(new ServerWebInputException("""
The content of comment must not be empty or contains unsafe HTML.\
"""));
}
return environmentFetcher.fetchComment() return environmentFetcher.fetchComment()
.flatMap(commentSetting -> { .flatMap(commentSetting -> {
if (Boolean.FALSE.equals(commentSetting.getEnable())) { if (Boolean.FALSE.equals(commentSetting.getEnable())) {

View File

@ -17,6 +17,7 @@ import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.retry.Retry; import reactor.util.retry.Retry;
@ -52,6 +53,13 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
@Override @Override
public Mono<Reply> create(String commentName, Reply reply) { public Mono<Reply> create(String commentName, Reply reply) {
if (reply.getSpec() == null
|| reply.getSpec().getContent() == null
|| !isSafeHtml(reply.getSpec().getContent())) {
return Mono.error(new ServerWebInputException("""
The content of reply must not be empty or contains unsafe HTML.\
"""));
}
return client.get(Comment.class, commentName) return client.get(Comment.class, commentName)
.flatMap(this::approveComment) .flatMap(this::approveComment)
.filter(comment -> isTrue(comment.getSpec().getApproved())) .filter(comment -> isTrue(comment.getSpec().getApproved()))