Spring 5.0.3 RequestRejectedException: Permintaan ditolak karena URL tidak dinormalisasi


89

Tidak yakin apakah ini bug dengan Spring 5.0.3 atau fitur baru untuk memperbaiki berbagai hal di pihak saya.

Setelah peningkatan, saya mendapatkan kesalahan ini. Menariknya kesalahan ini hanya ada di mesin lokal saya. Kode yang sama pada lingkungan pengujian dengan protokol HTTPS berfungsi dengan baik.

Melanjutkan ...

Alasan saya mendapatkan kesalahan ini adalah karena URL saya untuk memuat halaman JSP yang dihasilkan adalah /location/thisPage.jsp. Mengevaluasi kode request.getRequestURI()memberi saya hasil /WEB-INF/somelocation//location/thisPage.jsp. Jika saya memperbaiki URL halaman JSP ini location/thisPage.jsp, semuanya berfungsi dengan baik.

Jadi pertanyaan saya adalah, haruskah saya menghapus /dari JSPjalur dalam kode karena itulah yang diperlukan untuk maju. Atau Springtelah memperkenalkan bug karena satu-satunya perbedaan antara mesin saya dan lingkungan pengujian adalah protokol HTTPversus HTTPS.

 org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL was not normalized.
    at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:123)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:194)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)


1
Masalah direncanakan akan diselesaikan di 5.1.0; Saat ini 5.0.0 tidak memiliki masalah ini.
java_dude

Jawaban:


73

Dokumentasi Keamanan Musim Semi menyebutkan alasan pemblokiran // dalam permintaan.

Misalnya, ini bisa berisi urutan traversal jalur (seperti /../) atau beberapa garis miring (//) yang juga bisa menyebabkan kecocokan pola gagal. Beberapa kontainer menormalkannya sebelum melakukan pemetaan servlet, tetapi yang lain tidak. Untuk melindungi dari masalah seperti ini, FilterChainProxy menggunakan strategi HttpFirewall untuk memeriksa dan menggabungkan permintaan. Permintaan yang tidak dinormalisasi secara otomatis ditolak secara default, dan parameter jalur serta garis miring ganda dihapus untuk tujuan pencocokan.

Jadi ada dua solusi yang mungkin -

  1. hapus garis miring ganda (pendekatan yang disukai)
  2. Izinkan // di Keamanan Musim Semi dengan menyesuaikan StrictHttpFirewall menggunakan kode di bawah ini.

Langkah 1 Buat firewall khusus yang memungkinkan garis miring di URL.

@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowUrlEncodedSlash(true);    
    return firewall;
}

Langkah 2 Dan kemudian konfigurasikan kacang ini di websecurity

@Override
public void configure(WebSecurity web) throws Exception {
    //@formatter:off
    super.configure(web);
    web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
....
}

Langkah 2 adalah langkah opsional, Spring Boot hanya membutuhkan kacang untuk dideklarasikan jenisnya HttpFirewalldan akan secara otomatis mengkonfigurasinya dalam rantai filter.

Keamanan Musim Semi 5.4 Pembaruan

Dalam keamanan Musim Semi 5.4 dan di atasnya (Spring Boot> = 2.4.0), kita dapat menyingkirkan terlalu banyak log yang mengeluh tentang permintaan yang ditolak dengan membuat kacang di bawah ini.

import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;

@Bean
RequestRejectedHandler requestRejectedHandler() {
   return new HttpStatusRequestRejectedHandler();
}

Ya, keamanan jalur-traversal telah diperkenalkan. Itu fitur baru dan ini bisa menyebabkan masalah. Yang saya tidak terlalu yakin karena Anda melihatnya berfungsi di HTTPS dan bukan di HTTP. Saya lebih suka menunggu sampai bug ini teratasi jira.spring.io/browse/SPR-16419
java_dude

sangat mungkin menjadi bagian dari masalah kami ... tetapi ... pengguna tidak mengetik // jadi saya mencoba mencari tahu bagaimana / yang kedua itu ditambahkan di tempat pertama ... jika pegas menghasilkan url jstl itu tidak boleh menambahkan itu, atau menormalkannya setelah menambahkannya.
xenoterracide

5
Ini tidak benar-benar menyelesaikan solusi, setidaknya untuk Keamanan Musim Semi 5.1.1. Anda harus menggunakan DefaultHttpFirewall jika Anda membutuhkan URL dengan dua garis miring seperti a / b // c. Metode isNormalized tidak dapat dikonfigurasi atau diganti di StrictHttpFirewall.
Jason Winnebeck

Adakah kemungkinan seseorang dapat memberikan petunjuk tentang cara melakukan ini di Musim Semi saja dibandingkan dengan Boot?
sekun

29

setAllowUrlEncodedSlash(true)tidak berhasil untuk saya. Masih metode internal isNormalizedkembali falseketika memiliki garis miring ganda.

Saya menggantinya StrictHttpFirewalldengan hanya DefaultHttpFirewallmemiliki kode berikut:

@Bean
public HttpFirewall defaultHttpFirewall() {
    return new DefaultHttpFirewall();
}

Bekerja dengan baik untuk saya.
Ada risiko dengan menggunakan DefaultHttpFirewall?


1
Iya. Hanya karena Anda tidak bisa membuat kunci cadangan untuk teman sekamar Anda, bukan berarti Anda harus meletakkan satu-satunya kunci di bawah keset. Tidak disarankan. Keamanan tidak boleh diubah.
java_dude

18
@java_dude Hebat sekali karena Anda tidak memberikan info atau alasan sama sekali, hanya analogi yang samar-samar.
kaqqao

Pilihan lainnya adalah subclass StrictHttpFirewalluntuk memberikan sedikit lebih banyak kontrol atas penolakan URL, seperti yang dijelaskan dalam jawaban ini .
vallismortis

1
Ini berhasil untuk saya tetapi saya juga harus menambahkan ini di kacang XML saya:<sec:http-firewall ref="defaultHttpFirewall"/>
Jason Winnebeck

1
Apa implikasi menggunakan solusi ini?
Felipe Desiderati

10

Saya mengalami masalah yang sama dengan:

Versi Spring Boot = 1.5.10
Versi Spring Security = 4.2.4


Masalah terjadi di titik akhir, di mana ModelAndViewviewName ditentukan dengan garis miring sebelumnya . Contoh:

ModelAndView mav = new ModelAndView("/your-view-here");

Jika saya menghapus garis miring itu bekerja dengan baik. Contoh:

ModelAndView mav = new ModelAndView("your-view-here");

Saya juga melakukan beberapa pengujian dengan RedirectView dan tampaknya berfungsi dengan garis miring sebelumnya.


2
Itu bukan solusinya. Bagaimana jika ini adalah bug di sisi Spring. Jika mereka mengubahnya, Anda harus membatalkan semua perubahan itu lagi. Saya lebih suka menunggu sampai 5.1 karena itu ditandai untuk diselesaikan saat itu.
java_dude

1
Tidak, Anda tidak perlu mengembalikan perubahan karena menentukan viewName tanpa garis miring sebelumnya berfungsi dengan baik pada versi yang lebih lama.
Torsten Ojaperv

Itulah tepatnya masalahnya. Jika berfungsi dengan baik dan Anda tidak mengubah apa pun, Spring telah memperkenalkan bug. Jalur harus selalu dimulai dengan "/". Lihat dokumentasi musim semi apa pun. Lihat ini github.com/spring-projects/spring-security/issues/5007 & github.com/spring-projects/spring-security/issues/5044
java_dude

1
Ini menggigitku juga. Memperbarui semua ModelAndView tanpa '/' utama memperbaiki masalah
Nathan Perrier

jira.spring.io/browse/SPR-16740 Saya membuka bug, tetapi menghapus leading / belum menjadi perbaikan bagi saya, dan dalam banyak kasus kami hanya mengembalikan nama tampilan sebagai string (dari controller) . Perlu melihat tampilan pengalihan sebagai solusi.
xenoterracide


5

Dalam kasus saya, ditingkatkan dari spring-securiy-web 3.1.3 ke 4.2.12, defaultHttpFirewalldiubah dari DefaultHttpFirewallmenjadi StrictHttpFirewallsecara default. Jadi tentukan saja dalam konfigurasi XML seperti di bawah ini:

<bean id="defaultHttpFirewall" class="org.springframework.security.web.firewall.DefaultHttpFirewall"/>
<sec:http-firewall ref="defaultHttpFirewall"/>

ditetapkan HTTPFirewallsebagaiDefaultHttpFirewall


1
Harap tambahkan beberapa deskripsi ke kode Anda menjelaskan apa yang terjadi dan mengapa. Ini adalah latihan yang bagus. Jika tidak, jawaban Anda berisiko terhapus. Itu sudah ditandai sebagai kualitas rendah.
herrbischoff

3

Solusi di bawah ini adalah solusi yang bersih dan tidak membahayakan keamanan karena kami menggunakan firewall ketat yang sama.

Langkah-langkah untuk memperbaikinya adalah sebagai berikut:

LANGKAH 1: Buat Kelas yang menimpa StrictHttpFirewall seperti di bawah ini.

package com.biz.brains.project.security.firewall;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpMethod;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;

public class CustomStrictHttpFirewall implements HttpFirewall {
    private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());

    private static final String ENCODED_PERCENT = "%25";

    private static final String PERCENT = "%";

    private static final List<String> FORBIDDEN_ENCODED_PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E"));

    private static final List<String> FORBIDDEN_SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));

    private static final List<String> FORBIDDEN_FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F"));

    private static final List<String> FORBIDDEN_BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));

    private Set<String> encodedUrlBlacklist = new HashSet<String>();

    private Set<String> decodedUrlBlacklist = new HashSet<String>();

    private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();

    public CustomStrictHttpFirewall() {
        urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
        urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
        urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);

        this.encodedUrlBlacklist.add(ENCODED_PERCENT);
        this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
        this.decodedUrlBlacklist.add(PERCENT);
    }

    public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
        this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
    }

    public void setAllowedHttpMethods(Collection<String> allowedHttpMethods) {
        if (allowedHttpMethods == null) {
            throw new IllegalArgumentException("allowedHttpMethods cannot be null");
        }
        if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
            this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD;
        } else {
            this.allowedHttpMethods = new HashSet<>(allowedHttpMethods);
        }
    }

    public void setAllowSemicolon(boolean allowSemicolon) {
        if (allowSemicolon) {
            urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
        }
    }

    public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
        if (allowUrlEncodedSlash) {
            urlBlacklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
        }
    }

    public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
        if (allowUrlEncodedPeriod) {
            this.encodedUrlBlacklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
        } else {
            this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
        }
    }

    public void setAllowBackSlash(boolean allowBackSlash) {
        if (allowBackSlash) {
            urlBlacklistsRemoveAll(FORBIDDEN_BACKSLASH);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);
        }
    }

    public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
        if (allowUrlEncodedPercent) {
            this.encodedUrlBlacklist.remove(ENCODED_PERCENT);
            this.decodedUrlBlacklist.remove(PERCENT);
        } else {
            this.encodedUrlBlacklist.add(ENCODED_PERCENT);
            this.decodedUrlBlacklist.add(PERCENT);
        }
    }

    private void urlBlacklistsAddAll(Collection<String> values) {
        this.encodedUrlBlacklist.addAll(values);
        this.decodedUrlBlacklist.addAll(values);
    }

    private void urlBlacklistsRemoveAll(Collection<String> values) {
        this.encodedUrlBlacklist.removeAll(values);
        this.decodedUrlBlacklist.removeAll(values);
    }

    @Override
    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        rejectForbiddenHttpMethod(request);
        rejectedBlacklistedUrls(request);

        if (!isNormalized(request)) {
            request.setAttribute("isNormalized", new RequestRejectedException("The request was rejected because the URL was not normalized."));
        }

        String requestUri = request.getRequestURI();
        if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
            request.setAttribute("isNormalized",  new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters."));
        }
        return new FirewalledRequest(request) {
            @Override
            public void reset() {
            }
        };
    }

    private void rejectForbiddenHttpMethod(HttpServletRequest request) {
        if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
            return;
        }
        if (!this.allowedHttpMethods.contains(request.getMethod())) {
            request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the HTTP method \"" +
                    request.getMethod() +
                    "\" was not included within the whitelist " +
                    this.allowedHttpMethods));
        }
    }

    private void rejectedBlacklistedUrls(HttpServletRequest request) {
        for (String forbidden : this.encodedUrlBlacklist) {
            if (encodedUrlContains(request, forbidden)) {
                request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""));
            }
        }
        for (String forbidden : this.decodedUrlBlacklist) {
            if (decodedUrlContains(request, forbidden)) {
                request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""));
            }
        }
    }

    @Override
    public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
        return new FirewalledResponse(response);
    }

    private static Set<String> createDefaultAllowedHttpMethods() {
        Set<String> result = new HashSet<>();
        result.add(HttpMethod.DELETE.name());
        result.add(HttpMethod.GET.name());
        result.add(HttpMethod.HEAD.name());
        result.add(HttpMethod.OPTIONS.name());
        result.add(HttpMethod.PATCH.name());
        result.add(HttpMethod.POST.name());
        result.add(HttpMethod.PUT.name());
        return result;
    }

    private static boolean isNormalized(HttpServletRequest request) {
        if (!isNormalized(request.getRequestURI())) {
            return false;
        }
        if (!isNormalized(request.getContextPath())) {
            return false;
        }
        if (!isNormalized(request.getServletPath())) {
            return false;
        }
        if (!isNormalized(request.getPathInfo())) {
            return false;
        }
        return true;
    }

    private static boolean encodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getContextPath(), value)) {
            return true;
        }
        return valueContains(request.getRequestURI(), value);
    }

    private static boolean decodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getServletPath(), value)) {
            return true;
        }
        if (valueContains(request.getPathInfo(), value)) {
            return true;
        }
        return false;
    }

    private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
        int length = uri.length();
        for (int i = 0; i < length; i++) {
            char c = uri.charAt(i);
            if (c < '\u0020' || c > '\u007e') {
                return false;
            }
        }

        return true;
    }

    private static boolean valueContains(String value, String contains) {
        return value != null && value.contains(contains);
    }

    private static boolean isNormalized(String path) {
        if (path == null) {
            return true;
        }

        if (path.indexOf("//") > -1) {
            return false;
        }

        for (int j = path.length(); j > 0;) {
            int i = path.lastIndexOf('/', j - 1);
            int gap = j - i;

            if (gap == 2 && path.charAt(i + 1) == '.') {
                // ".", "/./" or "/."
                return false;
            } else if (gap == 3 && path.charAt(i + 1) == '.' && path.charAt(i + 2) == '.') {
                return false;
            }

            j = i;
        }

        return true;
    }

}

LANGKAH 2: Buat kelas FirewalledResponse

package com.biz.brains.project.security.firewall;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

class FirewalledResponse extends HttpServletResponseWrapper {
    private static final Pattern CR_OR_LF = Pattern.compile("\\r|\\n");
    private static final String LOCATION_HEADER = "Location";
    private static final String SET_COOKIE_HEADER = "Set-Cookie";

    public FirewalledResponse(HttpServletResponse response) {
        super(response);
    }

    @Override
    public void sendRedirect(String location) throws IOException {
        // TODO: implement pluggable validation, instead of simple blacklisting.
        // SEC-1790. Prevent redirects containing CRLF
        validateCrlf(LOCATION_HEADER, location);
        super.sendRedirect(location);
    }

    @Override
    public void setHeader(String name, String value) {
        validateCrlf(name, value);
        super.setHeader(name, value);
    }

    @Override
    public void addHeader(String name, String value) {
        validateCrlf(name, value);
        super.addHeader(name, value);
    }

    @Override
    public void addCookie(Cookie cookie) {
        if (cookie != null) {
            validateCrlf(SET_COOKIE_HEADER, cookie.getName());
            validateCrlf(SET_COOKIE_HEADER, cookie.getValue());
            validateCrlf(SET_COOKIE_HEADER, cookie.getPath());
            validateCrlf(SET_COOKIE_HEADER, cookie.getDomain());
            validateCrlf(SET_COOKIE_HEADER, cookie.getComment());
        }
        super.addCookie(cookie);
    }

    void validateCrlf(String name, String value) {
        if (hasCrlf(name) || hasCrlf(value)) {
            throw new IllegalArgumentException(
                    "Invalid characters (CR/LF) in header " + name);
        }
    }

    private boolean hasCrlf(String value) {
        return value != null && CR_OR_LF.matcher(value).find();
    }
}

LANGKAH 3: Buat Filter kustom untuk menyembunyikan RejectedException

package com.biz.brains.project.security.filter;

import java.io.IOException;
import java.util.Objects;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestRejectedExceptionFilter extends GenericFilterBean {

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            try {
                RequestRejectedException requestRejectedException=(RequestRejectedException) servletRequest.getAttribute("isNormalized");
                if(Objects.nonNull(requestRejectedException)) {
                    throw requestRejectedException;
                }else {
                    filterChain.doFilter(servletRequest, servletResponse);
                }
            } catch (RequestRejectedException requestRejectedException) {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
                HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
                log
                    .error(
                            "request_rejected: remote={}, user_agent={}, request_url={}",
                            httpServletRequest.getRemoteHost(),  
                            httpServletRequest.getHeader(HttpHeaders.USER_AGENT),
                            httpServletRequest.getRequestURL(), 
                            requestRejectedException
                    );

                httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }
}

LANGKAH 4: Tambahkan filter kustom ke rantai filter pegas dalam konfigurasi keamanan

@Override
protected void configure(HttpSecurity http) throws Exception {
     http.addFilterBefore(new RequestRejectedExceptionFilter(),
             ChannelProcessingFilter.class);
}

Sekarang menggunakan perbaikan di atas, kita dapat menangani RequestRejectedExceptiondengan halaman Error 404.


Terima kasih. Ini adalah pendekatan yang saya gunakan sementara untuk memungkinkan kami meningkatkan layanan mikro Java kami hingga semua aplikasi front-end ditingkatkan. Saya tidak memerlukan langkah 3 dan 4 agar berhasil mengizinkan '//' dianggap normal. Saya baru saja mengomentari kondisi yang memeriksa garis miring ganda di isNormalized dan kemudian mengonfigurasi kacang untuk menggunakan kelas CustomStrictHttpFirewall.
gtaborga

Apakah ada solusi yang lebih mudah melalui config? Tapi tanpa mematikan firewall ..
Prathamesh dhanawade

0

Dalam kasus saya, masalahnya disebabkan oleh tidak masuk dengan Postman, jadi saya membuka koneksi di tab lain dengan cookie sesi yang saya ambil dari header di sesi Chrome saya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.