La Hai
La Hai

Reputation: 303

How to prevent user from injecting field into form backing bean?

An user upload his comment via this form.

Thymeleaf

<form th:action="@{/comment}" th:id="form" method="post">
  <input type="hidden" th:name="productId.id" th:value="${product.id}">
  <textarea th:field="${comment.message}" class="comment"
                                  placeholder="Write comment here"></textarea>
  <input type="submit" id="submit" value="comment">
</form>

Actual HTML

<form action="/comment" id="form" method="post" class="">
  <input type="hidden" name="_csrf" value="f6b3f296-3284-4d2d-a2b2-0a9975f5e071">
  <input type="hidden" name="productId.id" value="38">
  <textarea class="comment" placeholder="Write comment here" id="message" name="message"></textarea>
  <input type="submit" id="submit" value="comment">
</form>

However if user overwrites the actual HTML like this, the product's name will be changed to "ABCD"

<form action="/comment" id="form" method="post" class=""><input type="hidden" name="_csrf" value="f6b3f296-3284-4d2d-a2b2-0a9975f5e071">
  <input type="hidden" name="productId" value="38">
  <input type="hidden" name="productId.name" value="ABCD">
  <textarea class="comment" placeholder="Write comment here" id="message" name="message"></textarea>
  <input type="submit" id="submit" value="comment">
</form>

I think what happened here is Spring queried the productId and it became managed Entity, and when the user set the name to be "ABCD", it would be saved.

Here is my solution:

Basically just use @Validated with a bunch of groups and put constraint with appropriate groups (UploadCommentValidation in this case) on every single field, which works but seems really messy especially when it gets big.

Example with upload comment above:

Comment entity

public class Comment implements Comparable<Comment> {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Null(groups = {UploadCommentValidation.class})
    @NotNull(groups = {DeleteCommentValidation.class, UpdateCommentValidation.class})
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "product_id", referencedColumnName = "id")
    @JsonBackReference
    @Valid
    @NotNull(groups = {UploadCommentValidation.class})
    @Null(groups = {DeleteCommentValidation.class, UpdateCommentValidation.class})
    private Product productId;

    @ManyToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    @JsonBackReference
    @Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
    private User userId;

    @Column(name = "message")
    @NotBlank(message = "please write a comment", groups = {UploadCommentValidation.class, UpdateCommentValidation.class})
    @Null(groups = {DeleteCommentValidation.class})
    private String message;

    @Column(name = "created_at", insertable = false, columnDefinition = "timestamp with time zone not null")
    @Temporal(TemporalType.TIMESTAMP)
    @Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
    private Calendar createdAt;

    @Column(name = "updated_at", columnDefinition = "timestamp with time zone not null")
    @Temporal(TemporalType.TIMESTAMP)
    @Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
    private Calendar updatedAt;

    @Override
    public int compareTo(Comment o) {
        return this.getId().compareTo(o.getId());
    }
}

Product entity

public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @NotNull(message = "product id null", groups = {AddOrderValidation.class, UploadCommentValidation.class})
    @Null(message = "bad request", groups = {ProductRegisterValidation.class})
    private Integer id;

    @NotBlank(message = "please fill in product name", groups = {ProductRegisterValidation.class})
    @Length(max = 255, message = "too long", groups = {ProductRegisterValidation.class})
    @Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
    @Column(name = "name")
    private String name;

    @Column(name = "price")
    @Positive(message = "the price must be non-negative", groups = {ProductRegisterValidation.class})
    @NotNull(message = "please fill in price", groups = {ProductRegisterValidation.class})
    @Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
    private Integer price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", referencedColumnName = "id")
    @Valid
    @NotNull(message = "please select category name", groups = {ProductRegisterValidation.class})
    @Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
    private Category categoryId;

    @NotBlank(message = "please fill in description", groups = {ProductRegisterValidation.class})
    @Length(max = 10000, message = "too long", groups = {ProductRegisterValidation.class})
    @Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
    @Column(name = "description")
    private String description;

    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
    private List<ProductImage> productImages;

    @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
    private Thumbnail thumbnail;

    @OneToMany(mappedBy = "productId", fetch = FetchType.LAZY)
    @JsonManagedReference
    @Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
    private List<Comment> comments;

    @OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
    @Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
    private List<Order> orders;
}

Any ideas how to do it the right way? This seems super messy!

UPDATE 1: This is my rest controller

@PostMapping("/comment")
    public ResponseEntity<Map<String, String>> commentResponseEntity(@Validated({UploadCommentValidation.class}) Comment comment, BindingResult result) {
        if (result.hasErrors()) {
            result.getAllErrors().forEach(System.out::println);
            return ResponseEntity.noContent().build();
        }
        User user = getUser();
        comment.setUserId(user);
        commentRepository.saveAndFlush(comment);
        Map<String, String> response = new HashMap<>();
        response.put("comment", comment.getMessage());
        response.put("user", user.getName());
        response.put("commentId", comment.getId().toString());
        return ResponseEntity.ok().body(response);
    }

Upvotes: 0

Views: 113

Answers (1)

Alan Hay
Alan Hay

Reputation: 23226

You can do this by registering an @InitBinder method

You can do this at the individual controller level or by registering a @ControllerAdvice to be applied to all, or a subset of all, controllers.

@InitBinder()
public void initBinder(WebDataBinder binder) {
    binder.setDisallowedFields(new String[] { "id", "version" });
}

Upvotes: 1

Related Questions