Skip to content
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

grpc: skip non-gRPC responses in the variant #5548

Merged
merged 1 commit into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion addOns/grpc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Fixed
- Do not try to decode non-gRPC responses when active scanning, which would lead to unnecessary warnings.

## [0.1.0] - 2024-06-11

Expand Down
2 changes: 2 additions & 0 deletions addOns/grpc/grpc.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ crowdin {
dependencies {
testImplementation(project(":testutils"))
implementation("io.grpc:grpc-protobuf:1.61.1")

testImplementation(libs.log4j.core)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably have some tests 😃

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.core.scanner.NameValuePair;
import org.parosproxy.paros.core.scanner.Variant;
import org.parosproxy.paros.network.HttpBody;
import org.parosproxy.paros.network.HttpHeader;
import org.parosproxy.paros.network.HttpMessage;

public class VariantGrpc implements Variant {
Expand All @@ -44,7 +46,7 @@ public class VariantGrpc implements Variant {

@Override
public void setMessage(HttpMessage msg) {
if (isValidGrpcMessage(msg)) {
if (isValidGrpcMessage(msg.getRequestHeader(), msg.getRequestBody())) {
try {
byte[] body = Base64.getDecoder().decode(msg.getRequestBody().getBytes());
byte[] payload = DecoderUtils.extractPayload(body);
Expand Down Expand Up @@ -92,9 +94,8 @@ private void parseContent(List<String> decodedList, String commonPrefixForNested
}
}

private boolean isValidGrpcMessage(HttpMessage msg) {
return msg.getRequestHeader().hasContentType("application/grpc")
&& !msg.getRequestBody().toString().isEmpty();
private static boolean isValidGrpcMessage(HttpHeader header, HttpBody body) {
return header.hasContentType("application/grpc") && !body.toString().isEmpty();
}

@Override
Expand Down Expand Up @@ -179,6 +180,10 @@ public String setEscapedParameter(

@Override
public void decodeResponseBody(HttpMessage msg) {
if (!isValidGrpcMessage(msg.getResponseHeader(), msg.getResponseBody())) {
return;
}

try {
byte[] body =
DecoderUtils.splitMessageBodyAndStatusCode(msg.getResponseBody().getBytes());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,28 @@
*/
package org.zaproxy.addon.grpc.internal;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.StringLayout;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.filter.BurstFilter;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.parosproxy.paros.core.scanner.NameValuePair;
Expand All @@ -32,11 +50,33 @@
import org.parosproxy.paros.network.HttpRequestHeader;

class VariantGrpcUnitTest {

private static final String LOG_MSG_ERROR_DECODING_RESPONSE =
"Error decoding the Response Body";
private static final String APPENDER_NAME = "ZAP-TestAppender";
private static Level originalLevel;
private static List<String> logMessages;

private VariantGrpc variantGrpc;

@BeforeEach
void setUp() {
variantGrpc = new VariantGrpc();
logMessages = new ArrayList<>();
}

@BeforeAll
static void initAppender() {
LoggerConfig rootLogger = LoggerContext.getContext().getConfiguration().getRootLogger();
rootLogger.addAppender(new TestAppender(VariantGrpcUnitTest::handleLog), null, null);
originalLevel = rootLogger.getLevel();
Configurator.setRootLevel(Level.ALL);
}

@AfterAll
static void removeAppender() {
LoggerContext.getContext().getConfiguration().getRootLogger().removeAppender(APPENDER_NAME);
Configurator.setRootLevel(originalLevel);
}

@Test
Expand Down Expand Up @@ -152,13 +192,70 @@ void shouldExtractParametersFromCorruptedGrpcMessage() throws HttpMalformedHeade
assertEquals(expectedParamList, variantGrpc.getParamList());
}

@Test
void shouldNotWarnWhenDecodingResponseOfNonGrpcMessage() {
// Given
HttpMessage httpMessage = new HttpMessage();
// When
variantGrpc.decodeResponseBody(httpMessage);
// Then
assertThat(logMessages, not(hasItem(startsWith(LOG_MSG_ERROR_DECODING_RESPONSE))));
}

@Test
void shouldWarnWhenFailedToDecodeMalformedResponseGrpcMessage() {
// Given
HttpMessage httpMessage = new HttpMessage();
setGrpcHeader(httpMessage.getResponseHeader());
httpMessage.getResponseBody().setBody("something not gRPC");
// When
variantGrpc.decodeResponseBody(httpMessage);
// Then
assertThat(logMessages, hasItem(startsWith(LOG_MSG_ERROR_DECODING_RESPONSE)));
}

private static void handleLog(String message) {
logMessages.add(message);
}

private static HttpMessage createHttpMessage(String encodedRequestBody)
throws HttpMalformedHeaderException {
HttpRequestHeader httpRequestHeader = new HttpRequestHeader();
httpRequestHeader.setMessage("POST /abc/xyz HTTP/1.1");
httpRequestHeader.setHeader(HttpHeader.CONTENT_TYPE, "application/grpc-web-text");
setGrpcHeader(httpRequestHeader);
HttpMessage httpMessage = new HttpMessage(httpRequestHeader);
httpMessage.setRequestBody(encodedRequestBody);
return httpMessage;
}

private static void setGrpcHeader(HttpHeader header) {
header.setHeader(HttpHeader.CONTENT_TYPE, "application/grpc-web-text");
}

static class TestAppender extends AbstractAppender {

private static final Property[] NO_PROPERTIES = {};

private final Consumer<String> logConsumer;

TestAppender(Consumer<String> logConsumer) {
super(
APPENDER_NAME,
BurstFilter.newBuilder().setMaxBurst(100).setLevel(Level.WARN).build(),
PatternLayout.newBuilder()
.withDisableAnsi(true)
.withCharset(StandardCharsets.UTF_8)
.withPattern("%m%n")
.build(),
true,
NO_PROPERTIES);
this.logConsumer = logConsumer;
start();
}

@Override
public void append(LogEvent event) {
logConsumer.accept(((StringLayout) getLayout()).toSerializable(event));
}
}
}
Loading