Hilla Cant upload file due to Forbidden response from backend

I’ve tried changing target to /connect/api/upload still no luck.
Since its returning 403 i thought it might be related to my security config. So i provided my spring security config class below.

I generated my hilla app yesterday using start.vaadin.com.

Cant upload file with backend response:

{
   "timestamp": "2025-01-02T07:51:30.179+00:00",
   "status": 403,
   "error": "Forbidden",
   "message": "Forbidden",
   "path": "/api/upload"
}

My upload component:

<Upload
  accept='application/pdf,.pdf'
  maxFiles={1}
  maxFileSize={10485760}
  target='/api/upload'
  className='w-full'
/>

Backend Controller

package com.mitpc.hr.services.file;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api")
@Log4j2
public class FileEndpoint {
    @Autowired
    private FileService fileService;

    @CrossOrigin
    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile uploadedFile) {
        log.info(uploadedFile.getOriginalFilename());
        return "works";
    }
}

Spring security config:

package com.mitpc.hr.security;

import com.nimbusds.jose.JWSAlgorithm;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/line-awesome/**/*.svg")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/api/upload")).anonymous());

        super.configure(http);
        setLoginView(http, "/login");
        setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode("CHANGED"), JWSAlgorithm.HS256.getName()), "CHANGED");
    }

}

Component on the web:
Screenshot 2025-01-02 160114

After debugging, i found out that csrf was blocking my upload request. So i added below line to my security config and error is resolved. But im not sure this is the correct fix. This code might have some security vulnerabilities.

http.csrf(csrf -> csrf.ignoringRequestMatchers("/api/upload"));
3 Likes

I had the same issue. Seems like POST requests are blocked by spring boot when csrf header is missing. Makes sense for safety.
For the vaadin-upload in Lit I added the csrf header before the request being send:

render() {
    return html`
        <vaadin-upload
            form-data-name="file"
            method="POST"
            target="/fileupload/recieve"
            @file-reject=${this.onFileRejected}
            @upload-request=${this.prepareForm}
            /* other events */
            >
        </vaadin-upload>
    `
}

private prepareForm(event: UploadRequestEvent) {
    /* [...] */
    const xhr = event.detail.xhr;
    const csrfHeader = getMeta("_csrf_header")
    const csrf = getMeta("_csrf")
    if (csrfHeader && csrf) {
        xhr.setRequestHeader(csrfHeader, csrf)
    }
}

export function getMeta(metaName: string) {
    const metas = document.getElementsByTagName('meta');
    for (const element of metas) {
        if (element.getAttribute('name') === metaName) {
            return element.getAttribute('content');
        }
    }
    return undefined;
}