mirror of https://github.com/halo-dev/halo
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
parent
59030f839a
commit
535fe01624
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
Loading…
Reference in New Issue