Skip to content

Commit

Permalink
Escape entity details queries (#793)
Browse files Browse the repository at this point in the history
* Prevent injection attacks from graph file

* Fix notification message when exactly 1 not found

* Update changelog

* Fix isNotEmptyIfString

* Fix isValidRdfEdgeIdIfSparql
  • Loading branch information
kmcginnes authored Feb 17, 2025
1 parent 33bd7a0 commit 01b4fd6
Show file tree
Hide file tree
Showing 20 changed files with 849 additions and 45 deletions.
3 changes: 2 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
[#770](https://github.com/aws/graph-explorer/pull/770),
[#775](https://github.com/aws/graph-explorer/pull/775),
[#781](https://github.com/aws/graph-explorer/pull/781),
[#786](https://github.com/aws/graph-explorer/pull/786))
[#786](https://github.com/aws/graph-explorer/pull/786),
[#793](https://github.com/aws/graph-explorer/pull/793))
- **Updated** UI labels to refer to node & edge "labels" instead of "types"
([#766](https://github.com/aws/graph-explorer/pull/766))
- **Improved** neighbor count retrieval to be more efficient
Expand Down
67 changes: 67 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/edgeDetails.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createRandomEdge, createRandomVertex } from "@/utils/testing";
import { edgeDetails } from "./edgeDetails";
import { Edge } from "@/core";

describe("edgeDetails", () => {
it("should return the correct edge details", async () => {
const edge = createRandomEdge(createRandomVertex(), createRandomVertex());
edge.__isFragment = false;
const response = createGremlinResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

const result = await edgeDetails(mockFetch, {
edgeId: edge.id,
});

expect(result.edge).toEqual(edge);
});
});

function createGremlinResponseFromEdge(edge: Edge) {
return {
result: {
data: {
"@type": "g:List",
"@value": [
{
"@type": "g:Edge",
"@value": {
id: edge.id,
label: edge.type,
inV: edge.target,
outV: edge.source,
inVLabel: edge.targetType,
outVLabel: edge.sourceType,
properties: createProperties(edge.attributes),
},
},
],
},
},
};
}

function createProperties(attributes: Edge["attributes"]) {
const mapped = Object.entries(attributes).map(([key, value]) => ({
"@type": "g:EdgeProperty",
"@value": {
key,
value:
typeof value === "string"
? value
: {
"@type": "g:Int64",
"@value": value,
},
},
}));

const result = {} as Record<string, any>;
mapped.forEach(prop => {
result[prop["@value"].key] = prop;
});

return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createRandomVertex } from "@/utils/testing";
import { vertexDetails } from "./vertexDetails";
import { Vertex } from "@/core";

describe("vertexDetails", () => {
it("should return the correct vertex details", async () => {
const vertex = createRandomVertex();

// Align with the gremlin mapper
vertex.types = [vertex.type];
vertex.__isFragment = false;

const response = createGremlinResponseFromVertex(vertex);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

const result = await vertexDetails(mockFetch, {
vertexId: vertex.id,
});

expect(result.vertex).toEqual(vertex);
});
});

function createGremlinResponseFromVertex(vertex: Vertex) {
return {
result: {
data: {
"@type": "g:List",
"@value": [
{
"@type": "g:Vertex",
"@value": {
id: vertex.id,
label: vertex.type,
properties: createProperties(vertex.attributes),
},
},
],
},
},
};
}

function createProperties(attributes: Vertex["attributes"]) {
const mapped = Object.entries(attributes).map(([key, value]) => ({
"@type": "g:VertexProperty",
"@value": {
label: key,
value:
typeof value === "string"
? value
: {
"@type": "g:Int64",
"@value": value,
},
},
}));

const result = {} as Record<string, any>;
mapped.forEach(prop => {
result[prop["@value"].label] = [prop];
});

return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createRandomEdge, createRandomVertex } from "@/utils/testing";
import { edgeDetails } from "./edgeDetails";
import { Edge } from "@/core";

describe("edgeDetails", () => {
it("should return the edge details", async () => {
const edge = createRandomEdge(createRandomVertex(), createRandomVertex());

const response = createResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

const result = await edgeDetails(mockFetch, { edgeId: edge.id });

expect(result.edge).toEqual(edge);
});
});

function createResponseFromEdge(edge: Edge) {
return {
results: [
{
edge: {
"~id": edge.id,
"~type": edge.type,
"~start": edge.source,
"~end": edge.target,
"~properties": edge.attributes,
},
sourceLabels: [edge.sourceType],
targetLabels: [edge.targetType],
},
],
};
}
6 changes: 5 additions & 1 deletion packages/graph-explorer/src/connector/openCypher/idParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ import { EdgeId, getRawId, VertexId } from "@/core";

/** Formats the ID parameter for an openCypher query based on the ID type. */
export function idParam(id: VertexId | EdgeId) {
return `"${getRawId(id)}"`;
const rawId = getRawId(id);
if (typeof rawId !== "string") {
throw new Error(`Invalid ID type: ${typeof rawId}`);
}
return `"${rawId}"`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createRandomVertex } from "@/utils/testing";
import { vertexDetails } from "./vertexDetails";
import { Vertex } from "@/core";

describe("vertexDetails", () => {
it("should return the vertex details", async () => {
const vertex = createRandomVertex();
vertex.types = [vertex.type];

const response = createResponseFromVertex(vertex);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

const result = await vertexDetails(mockFetch, { vertexId: vertex.id });

expect(result.vertex).toEqual(vertex);
});
});

function createResponseFromVertex(vertex: Vertex) {
return {
results: [
{
vertex: {
"~id": vertex.id,
"~labels": [vertex.type],
"~properties": vertex.attributes,
},
},
],
};
}
20 changes: 20 additions & 0 deletions packages/graph-explorer/src/connector/sparql/createRdfEdgeId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createEdgeId, VertexId } from "@/core";

/**
* Combines the triple that makes up an edge into a single string.
*
* The format is:
* {source}-[{predicate}]->{target}
*
* @param source The source resource URI
* @param predicate The predicate URI
* @param target The target resource URI
* @returns A string that represents the relationship between the source and target
*/
export function createRdfEdgeId(
source: string | VertexId,
predicate: string,
target: string | VertexId
) {
return createEdgeId(`${source}-[${predicate}]->${target}`);
}
108 changes: 108 additions & 0 deletions packages/graph-explorer/src/connector/sparql/edgeDetails.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Edge, EdgeId } from "@/core";
import {
createRandomEdgeForRdf,
createRandomVertexForRdf,
} from "@/utils/testing";
import { edgeDetails } from "./edgeDetails";
import { createRandomUrlString } from "@shared/utils/testing";

describe("edgeDetails", () => {
it("should return the edge details", async () => {
const edge = createRandomEdgeForRdf(
createRandomVertexForRdf(),
createRandomVertexForRdf()
);
const response = createResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));
const result = await edgeDetails(mockFetch, { edgeId: edge.id });
expect(result.edge).toEqual(edge);
});

it("should throw an error when the edge ID is not in the RDF edge ID format", async () => {
const edge = createRandomEdgeForRdf(
createRandomVertexForRdf(),
createRandomVertexForRdf()
);
// Missing the brackets
edge.id = `${edge.source}-${edge.type}->${edge.target}` as EdgeId;
const response = createResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

await expect(edgeDetails(mockFetch, { edgeId: edge.id })).rejects.toThrow(
"Invalid RDF edge ID"
);
});

it("should throw an error when the source vertex ID doesn't match the response", async () => {
const edge = createRandomEdgeForRdf(
createRandomVertexForRdf(),
createRandomVertexForRdf()
);
edge.id =
`${createRandomUrlString()}-[${edge.type}]->${edge.target}` as EdgeId;
const response = createResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

await expect(edgeDetails(mockFetch, { edgeId: edge.id })).rejects.toThrow(
"Edge type not found in bindings"
);
});

it("should throw an error when the target vertex ID doesn't match the response", async () => {
const edge = createRandomEdgeForRdf(
createRandomVertexForRdf(),
createRandomVertexForRdf()
);
edge.id =
`${edge.source}-[${edge.type}]->${createRandomUrlString()}` as EdgeId;
const response = createResponseFromEdge(edge);
const mockFetch = vi
.fn()
.mockImplementation(() => Promise.resolve(response));

await expect(edgeDetails(mockFetch, { edgeId: edge.id })).rejects.toThrow(
"Edge type not found in bindings"
);
});
});

function createResponseFromEdge(edge: Edge) {
const source = edge.source;
const target = edge.target;

return {
head: {
vars: ["resource", "type"],
},
results: {
bindings: [
{
resource: {
type: "uri",
value: source,
},
type: {
type: "uri",
value: edge.sourceType,
},
},
{
resource: {
type: "uri",
value: target,
},
type: {
type: "uri",
value: edge.targetType,
},
},
],
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export async function edgeDetails(
const edge = <Edge>{
entityType: "edge",
id: request.edgeId,
idType: "string",
type: predicate,
source: source,
sourceType,
Expand Down
6 changes: 5 additions & 1 deletion packages/graph-explorer/src/connector/sparql/idParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ import { EdgeId, getRawId, VertexId } from "@/core";

/** Formats the ID parameter for a sparql query based on the ID type. */
export function idParam(id: VertexId | EdgeId) {
return `<${getRawId(id)}>`;
const rawId = getRawId(id);
if (typeof rawId !== "string") {
throw new Error("ID must be a URI");
}
return `<${rawId}>`;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEdgeId, createVertexId, Edge, getRawId, VertexId } from "@/core";
import { createVertexId, Edge, VertexId } from "@/core";
import { RawValue } from "../types";
import { createRdfEdgeId } from "../createRdfEdgeId";

export type IncomingPredicate = {
subject: RawValue;
Expand All @@ -17,7 +18,7 @@ const mapIncomingToEdge = (

return {
entityType: "edge",
id: createEdgeId(`${sourceUri}-[${predicate}]->${getRawId(resourceURI)}`),
id: createRdfEdgeId(sourceUri, predicate, resourceURI),
type: predicate,
source: createVertexId(sourceUri),
sourceType: result.subjectClass.value,
Expand Down
Loading

0 comments on commit 01b4fd6

Please sign in to comment.