Skip to content

Commit

Permalink
Merge pull request #5843 from kingthorin/gql-td
Browse files Browse the repository at this point in the history
graphql: Add tech detection for GraphQL Engines
  • Loading branch information
ricekot authored Dec 14, 2024
2 parents a1f83eb + 03a0902 commit 0e2491b
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 28 deletions.
1 change: 1 addition & 0 deletions addOns/graphql/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Hot Chocolate
- Inigo
- Support for importing an introspection query response from a file (Issue 8569).
- If the Tech Detection (Wappalyzer) add-on is installed and a GraphQL engine is successfully fingerprinted, it is added to the Technology tab/data.

## [0.25.0] - 2024-09-24
### Changed
Expand Down
14 changes: 14 additions & 0 deletions addOns/graphql/graphql.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ zapAddOn {
}
}
}

register("org.zaproxy.addon.graphql.techdetection.ExtensionTechDetectionGraphQl") {
classnames {
allowed.set(listOf("org.zaproxy.addon.graphql.techdetection"))
}
dependencies {
addOns {
register("wappalyzer") {
version.set(">= 21.44.0")
}
}
}
}
}
}

Expand All @@ -61,6 +74,7 @@ dependencies {
zapAddOn("automation")
zapAddOn("commonlib")
zapAddOn("spider")
zapAddOn("wappalyzer")

implementation("com.graphql-java:graphql-java:22.3")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2024 The ZAP Development Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.zaproxy.addon.graphql;

import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine;

public interface DiscoveredGraphQlEngineHandler {

void process(DiscoveredGraphQlEngine engine);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -42,6 +43,7 @@
import org.parosproxy.paros.network.HttpSender;
import org.zaproxy.addon.commonlib.ExtensionCommonlib;
import org.zaproxy.addon.commonlib.ValueProvider;
import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine;
import org.zaproxy.zap.extension.alert.ExampleAlertProvider;
import org.zaproxy.zap.extension.script.ExtensionScript;
import org.zaproxy.zap.view.ZapMenuItem;
Expand Down Expand Up @@ -295,8 +297,16 @@ public boolean handleFile(File file) {

@Override
public List<Alert> getExampleAlerts() {
URI uri = null;
try {
uri = new URI("https://example.com", false);
} catch (URIException | NullPointerException e) {
// Ignore
}
return List.of(
GraphQlParser.createIntrospectionAlert().build(),
GraphQlFingerprinter.createFingerprintingAlert("example").build());
GraphQlFingerprinter.createFingerprintingAlert(
new DiscoveredGraphQlEngine("example", uri))
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import org.apache.commons.httpclient.URI;
Expand All @@ -45,6 +47,8 @@ public class GraphQlFingerprinter {
private static final Logger LOGGER = LogManager.getLogger(GraphQlFingerprinter.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private static List<DiscoveredGraphQlEngineHandler> handlers;

private final Requestor requestor;
private final Map<String, HttpMessage> queryCache;

Expand Down Expand Up @@ -98,7 +102,12 @@ public void fingerprint() {
for (var fingerprinter : fingerprinters.entrySet()) {
try {
if (fingerprinter.getValue().getAsBoolean()) {
raiseFingerprintingAlert(fingerprinter.getKey());
DiscoveredGraphQlEngine discoveredGraphQlEngine =
new DiscoveredGraphQlEngine(
fingerprinter.getKey(),
lastQueryMsg.getRequestHeader().getURI());
handleDetectedEngine(discoveredGraphQlEngine);
raiseFingerprintingAlert(discoveredGraphQlEngine);
break;
}
} catch (Exception e) {
Expand All @@ -108,6 +117,16 @@ public void fingerprint() {
queryCache.clear();
}

private static void handleDetectedEngine(DiscoveredGraphQlEngine discoveredGraphQlEngine) {
for (DiscoveredGraphQlEngineHandler handler : handlers) {
try {
handler.process(discoveredGraphQlEngine);
} catch (Exception ex) {
LOGGER.error("Unable to handle: {}", discoveredGraphQlEngine.getName(), ex);
}
}
}

void sendQuery(String query) {
lastQueryMsg =
queryCache.computeIfAbsent(
Expand Down Expand Up @@ -150,18 +169,18 @@ boolean errorContains(String substring, String errorField) {
return false;
}

static Alert.Builder createFingerprintingAlert(String engineId) {
final String enginePrefix = "graphql.engine." + engineId + ".";
static Alert.Builder createFingerprintingAlert(
DiscoveredGraphQlEngine discoveredGraphQlEngine) {
return Alert.builder()
.setPluginId(ExtensionGraphQl.TOOL_ALERT_ID)
.setAlertRef(FINGERPRINTING_ALERT_REF)
.setName(Constant.messages.getString("graphql.fingerprinting.alert.name"))
.setDescription(
Constant.messages.getString(
"graphql.fingerprinting.alert.desc",
Constant.messages.getString(enginePrefix + "name"),
Constant.messages.getString(enginePrefix + "technologies")))
.setReference(Constant.messages.getString(enginePrefix + "docsUrl"))
discoveredGraphQlEngine.getName(),
discoveredGraphQlEngine.getTechnologies()))
.setReference(discoveredGraphQlEngine.getDocsUrl())
.setConfidence(Alert.CONFIDENCE_HIGH)
.setRisk(Alert.RISK_INFO)
.setCweId(205)
Expand All @@ -170,14 +189,15 @@ static Alert.Builder createFingerprintingAlert(String engineId) {
.setTags(FINGERPRINTING_ALERT_TAGS);
}

private void raiseFingerprintingAlert(String engineId) {
void raiseFingerprintingAlert(DiscoveredGraphQlEngine discoveredGraphQlEngine) {
var extAlert =
Control.getSingleton().getExtensionLoader().getExtension(ExtensionAlert.class);
if (extAlert == null) {
return;
}

Alert alert =
createFingerprintingAlert(engineId)
createFingerprintingAlert(discoveredGraphQlEngine)
.setEvidence(matchedString)
.setMessage(lastQueryMsg)
.setUri(requestor.getEndpointUrl().toString())
Expand Down Expand Up @@ -618,4 +638,49 @@ private boolean checkWpGraphQlEngine() {
}
return false;
}

public static void addEngineHandler(DiscoveredGraphQlEngineHandler handler) {
if (handlers == null) {
handlers = new ArrayList<>();
}
handlers.add(handler);
}

public static void resetHandlers() {
handlers = null;
}

public static class DiscoveredGraphQlEngine {
private static final String PREFIX = "graphql.engine.";
private String enginePrefix;
private String name;
private String docsUrl;
private String technologies;
private URI uri;

public DiscoveredGraphQlEngine(String engineId, URI uri) {
this.enginePrefix = PREFIX + engineId + ".";

this.name = Constant.messages.getString(enginePrefix + "name");
this.docsUrl = Constant.messages.getString(enginePrefix + "docsUrl");
this.technologies = Constant.messages.getString(enginePrefix + "technologies");
this.uri = uri;
}

public String getName() {
return name;
}

public String getDocsUrl() {
return docsUrl;
}

public String getTechnologies() {
return technologies;
}

public URI getUri() {
return uri;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2024 The ZAP Development Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.zaproxy.addon.graphql.techdetection;

import java.util.List;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.extension.Extension;
import org.parosproxy.paros.extension.ExtensionAdaptor;
import org.parosproxy.paros.extension.ExtensionHook;
import org.zaproxy.addon.graphql.GraphQlFingerprinter;
import org.zaproxy.addon.graphql.GraphQlFingerprinter.DiscoveredGraphQlEngine;
import org.zaproxy.zap.extension.wappalyzer.Application;
import org.zaproxy.zap.extension.wappalyzer.ApplicationMatch;
import org.zaproxy.zap.extension.wappalyzer.ExtensionWappalyzer;

public class ExtensionTechDetectionGraphQl extends ExtensionAdaptor {

public static final String NAME = "ExtensionTechDetectionGraphQl";

private static final List<Class<? extends Extension>> DEPENDENCIES =
List.of(ExtensionWappalyzer.class);

private static ExtensionWappalyzer extTech;

public ExtensionTechDetectionGraphQl() {
super(NAME);
}

@Override
public String getUIName() {
return Constant.messages.getString("graphql.techdetection.name");
}

@Override
public String getDescription() {
return Constant.messages.getString("graphql.techdetection.desc");
}

@Override
public List<Class<? extends Extension>> getDependencies() {
return DEPENDENCIES;
}

@Override
public void hook(ExtensionHook extensionHook) {
super.hook(extensionHook);
GraphQlFingerprinter.addEngineHandler(ExtensionTechDetectionGraphQl::addApp);
}

@Override
public boolean canUnload() {
return true;
}

@Override
public void unload() {
GraphQlFingerprinter.resetHandlers();
}

private static ApplicationMatch getAppForEngine(DiscoveredGraphQlEngine engine) {
Application gqlEngine = new Application();
gqlEngine.setName(engine.getName());
gqlEngine.setCategories(List.of("GraphQL Engine"));
gqlEngine.setWebsite(engine.getDocsUrl());
gqlEngine.setImplies(List.of(engine.getTechnologies()));

return new ApplicationMatch(gqlEngine);
}

private static void addApp(DiscoveredGraphQlEngine engine) {
getExtTech().addApplicationsToSite(engine.getUri(), getAppForEngine(engine));
}

private static ExtensionWappalyzer getExtTech() {
if (extTech == null) {
extTech =
Control.getSingleton()
.getExtensionLoader()
.getExtension(ExtensionWappalyzer.class);
}
return extTech;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ <h1 id="id-50007">GraphQL Alerts</h1>
<td><a href="https://www.zaproxy.org/docs/alerts/50007-2/">50007-2</a>
<td>GraphQL Server Implementation Identified
<td>This alert is raised when the GraphQL implementation used by the server is identified. It utilises
fingerprinting techniques adapted from the tool <a href="https://github.com/dolevf/graphw00f">graphw00f</a>.
fingerprinting techniques adapted from the tool <a href="https://github.com/dolevf/graphw00f">graphw00f</a>.<br>
<strong>Note:</strong> If the Tech Detection (Wappalyzer) add-on is installed the fingerprinter will also add identified GraphQL Engines to the Technology tab/data.
<td><a href="https://github.com/zaproxy/zap-extensions/tree/main/addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlFingerprinter.java">GraphQlFingerprinter.java</a>
</table>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,8 @@ graphql.options.value.split.rootField = Each Field of an Operation
graphql.spider.desc = GraphQL Spider Integration
graphql.spider.name = GraphQL Spider

graphql.techdetection.desc = GraphQL Technology Detection Integration
graphql.techdetection.name = GraphQL Tech Detection

graphql.topmenu.import.importgraphql = Import a GraphQL Schema
graphql.topmenu.import.importgraphql.tooltip = Specify a GraphQL endpoint and optionally a GraphQL schema file to import.
Loading

0 comments on commit 0e2491b

Please sign in to comment.