-
Notifications
You must be signed in to change notification settings - Fork 77
BREAKING CHANGE: do not allow simple requests for login #4823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
cd54510
verify content type for login
achmelo 3dadd57
expect gateway to reject login with incorrect content type
achmelo a2dad4a
pass string as body
achmelo c3bab71
No body present: fall back to empty bytes without setting CACHED_BODY…
achmelo d9a3a2c
Merge branch 'v3.x.x' into reboot/login-csrf
achmelo b2c8947
validate fetch site header
achmelo 0ca1beb
validate fetch site header
achmelo d0690eb
test cross origin requests
achmelo 6c8ca33
continue only when CORS enabled
achmelo 5f8f0e0
remove import
achmelo 793f7f8
update tests
achmelo 6f40ab2
refactor
achmelo 0b36092
Merge branch 'v3.x.x' into reboot/login-csrf
achmelo 1ee346b
hide feature behind flag
achmelo 64eb060
Merge branch 'v3.x.x' into reboot/login-csrf
taban03 a3bb73a
Apply suggestions from code review
achmelo 79f7279
update javadoc and unit tests
achmelo 6361a0c
Merge remote-tracking branch 'origin/v3.x.x' into reboot/login-csrf
achmelo a43a0d6
update javadoc
achmelo 736c89a
imports
achmelo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
...ecurity-common/src/main/java/org/zowe/apiml/security/common/filter/ContentTypeFilter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /* | ||
| * This program and the accompanying materials are made available under the terms of the | ||
| * Eclipse Public License v2.0 which accompanies this distribution, and is available at | ||
| * https://www.eclipse.org/legal/epl-v20.html | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Copyright Contributors to the Zowe Project. | ||
| */ | ||
|
|
||
| package org.zowe.apiml.security.common.filter; | ||
|
|
||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.NonNull; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.InvalidMediaTypeException; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Rejects requests that carry a body with HTTP 415 unless they declare a {@code Content-Type} | ||
| * equals to {@code application/json}. Bodyless requests (e.g. cert or Basic-Auth login, | ||
| * logout) pass through unchecked, since they have nothing to be misinterpreted as JSON. | ||
| * <p> | ||
| * Body presence is determined from the {@code Content-Length}/{@code Transfer-Encoding} headers | ||
| * rather than by reading the request body, since the body stream is only safely readable once | ||
| * downstream (e.g. by {@code LoginFilter}). | ||
| */ | ||
| public class ContentTypeFilter extends OncePerRequestFilter { | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { | ||
| boolean hasBody = request.getContentLengthLong() > 0 || request.getHeader(HttpHeaders.TRANSFER_ENCODING) != null; | ||
| if (hasBody && !hasJsonContentType(request)) { | ||
| response.setStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()); | ||
| return; | ||
| } | ||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private boolean hasJsonContentType(HttpServletRequest request) { | ||
| String contentType = request.getContentType(); | ||
| if (contentType == null) { | ||
| return false; | ||
| } | ||
| try { | ||
| MediaType mediaType = MediaType.parseMediaType(contentType); | ||
| return MediaType.APPLICATION_JSON.equalsTypeAndSubtype(mediaType); | ||
| } catch (InvalidMediaTypeException e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| } |
107 changes: 107 additions & 0 deletions
107
...ity-common/src/test/java/org/zowe/apiml/security/common/filter/ContentTypeFilterTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /* | ||
| * This program and the accompanying materials are made available under the terms of the | ||
| * Eclipse Public License v2.0 which accompanies this distribution, and is available at | ||
| * https://www.eclipse.org/legal/epl-v20.html | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Copyright Contributors to the Zowe Project. | ||
| */ | ||
|
|
||
| package org.zowe.apiml.security.common.filter; | ||
|
|
||
| import jakarta.servlet.ServletException; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Nested; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.mock.web.MockFilterChain; | ||
| import org.springframework.mock.web.MockHttpServletRequest; | ||
| import org.springframework.mock.web.MockHttpServletResponse; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
|
|
||
| class ContentTypeFilterTest { | ||
|
|
||
| private ContentTypeFilter filter; | ||
| private MockHttpServletResponse response; | ||
| private MockFilterChain chain; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| filter = new ContentTypeFilter(); | ||
| response = new MockHttpServletResponse(); | ||
| chain = new MockFilterChain(); | ||
| } | ||
|
|
||
| @Nested | ||
| class WhenRequestHasBody { | ||
|
|
||
| @Test | ||
| void givenJsonContentType_thenRequestProceeds() throws ServletException, IOException { | ||
| var request = new MockHttpServletRequest("POST", "/auth/login"); | ||
| request.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| request.setContent("{}".getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| filter.doFilter(request, response, chain); | ||
|
|
||
| assertEquals(request, chain.getRequest()); | ||
| } | ||
|
|
||
| @Test | ||
| void givenNoContentType_thenRejectedWithUnsupportedMediaType() throws ServletException, IOException { | ||
| var request = new MockHttpServletRequest("POST", "/auth/login"); | ||
| request.setContent("{}".getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| filter.doFilter(request, response, chain); | ||
|
|
||
| assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), response.getStatus()); | ||
| assertNull(chain.getRequest()); | ||
| } | ||
|
|
||
| @Test | ||
| void givenNonJsonContentType_thenRejectedWithUnsupportedMediaType() throws ServletException, IOException { | ||
| var request = new MockHttpServletRequest("POST", "/auth/login"); | ||
| request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); | ||
| request.setContent("username=a&password=b".getBytes(StandardCharsets.UTF_8)); | ||
|
|
||
| filter.doFilter(request, response, chain); | ||
|
|
||
| assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), response.getStatus()); | ||
| assertNull(chain.getRequest()); | ||
| } | ||
|
|
||
| @Test | ||
| void givenChunkedTransferEncodingAndNoContentType_thenRejectedWithUnsupportedMediaType() throws ServletException, IOException { | ||
| var request = new MockHttpServletRequest("POST", "/auth/login"); | ||
| request.addHeader(HttpHeaders.TRANSFER_ENCODING, "chunked"); | ||
|
|
||
| filter.doFilter(request, response, chain); | ||
|
|
||
| assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), response.getStatus()); | ||
| assertNull(chain.getRequest()); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @Nested | ||
| class WhenRequestHasNoBody { | ||
|
|
||
| @Test | ||
| void givenNoContentTypeAndNoBody_thenRequestProceeds() throws ServletException, IOException { | ||
| var request = new MockHttpServletRequest("POST", "/auth/logout"); | ||
|
|
||
| filter.doFilter(request, response, chain); | ||
|
|
||
| assertEquals(request, chain.getRequest()); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
apiml/src/main/java/org/zowe/apiml/filter/ContentTypeFilter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /* | ||
| * This program and the accompanying materials are made available under the terms of the | ||
| * Eclipse Public License v2.0 which accompanies this distribution, and is available at | ||
| * https://www.eclipse.org/legal/epl-v20.html | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Copyright Contributors to the Zowe Project. | ||
| */ | ||
|
|
||
| package org.zowe.apiml.filter; | ||
|
|
||
| import org.springframework.core.Ordered; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.web.server.ServerWebExchange; | ||
| import org.springframework.web.server.WebFilter; | ||
| import org.springframework.web.server.WebFilterChain; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| /** | ||
| * Rejects requests that carry a body with HTTP 415 unless they declare a {@code Content-Type} | ||
| * equals to {@code application/json}. Bodyless requests (e.g. cert or Basic-Auth login, | ||
| * logout) pass through unchecked, since they have nothing to be misinterpreted as JSON. | ||
| * <p> | ||
| * Must run after {@link CachedBodyFilter}: whether a body was actually sent is determined from | ||
| * {@link CachedBodyFilter#CACHED_BODY_ATTR} rather than the {@code Content-Length} header, which | ||
| * is absent for chunked/proxied requests. | ||
| */ | ||
| public class ContentTypeFilter implements WebFilter, Ordered { | ||
|
|
||
| @Override | ||
| public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | ||
| boolean hasBody = exchange.getAttribute(CachedBodyFilter.CACHED_BODY_ATTR) != null; | ||
| if (hasBody) { | ||
| MediaType contentType = exchange.getRequest().getHeaders().getContentType(); | ||
| if (contentType == null || !MediaType.APPLICATION_JSON.equalsTypeAndSubtype(contentType)) { | ||
| exchange.getResponse().setStatusCode(HttpStatus.UNSUPPORTED_MEDIA_TYPE); | ||
| return exchange.getResponse().setComplete(); | ||
| } | ||
| } | ||
| return chain.filter(exchange); | ||
| } | ||
|
|
||
| @Override | ||
| public int getOrder() { | ||
| return Ordered.HIGHEST_PRECEDENCE + 1; | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.