diff --git a/javascript/ql/lib/change-notes/2025-02-25-react-relay.md b/javascript/ql/lib/change-notes/2025-02-25-react-relay.md new file mode 100644 index 000000000000..822f429f62a4 --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-02-25-react-relay.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added support for the `react-relay` library. diff --git a/javascript/ql/lib/ext/react-relay-threat.model.yml b/javascript/ql/lib/ext/react-relay-threat.model.yml new file mode 100644 index 000000000000..47b1d086533e --- /dev/null +++ b/javascript/ql/lib/ext/react-relay-threat.model.yml @@ -0,0 +1,15 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + - ["react-relay", "Member[useFragment].ReturnValue", "response"] + - ["react-relay", "Member[useLazyLoadQuery].ReturnValue", "response"] + - ["react-relay", "Member[usePreloadedQuery].ReturnValue", "response"] + - ["react-relay", "Member[useClientQuery].ReturnValue", "response"] + - ["react-relay", "Member[useRefetchableFragment].ReturnValue.Member[0]", "response"] + - ["react-relay", "Member[usePaginationFragment].ReturnValue.Member[data]", "response"] + - ["react-relay", "Member[useMutation].ReturnValue.Member[0].Argument[0].Member[onCompleted].Parameter[0]", "response"] + - ["react-relay", "Member[useSubscription].Argument[0].Member[onNext].Parameter[0]", "response"] + - ["react-relay", "Member[fetchQuery].ReturnValue.Member[subscribe].Argument[0].Member[next].Parameter[0]", "response"] + - ["relay-runtime", "Member[readFragment].ReturnValue", "response"] diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected index f83c9f40b315..b2be2a7bcc2e 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected @@ -1,5 +1,15 @@ #select | test.jsx:27:29:27:32 | data | test.jsx:5:28:5:63 | fetch(" ... ntent") | test.jsx:27:29:27:32 | data | Cross-site scripting vulnerability due to $@. | test.jsx:5:28:5:63 | fetch(" ... ntent") | user-provided value | +| testReactRelay.tsx:7:43:7:58 | commentData.text | testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | testReactRelay.tsx:7:43:7:58 | commentData.text | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | user-provided value | +| testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | user-provided value | +| testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | user-provided value | +| testReactRelay.tsx:38:49:38:52 | data | testReactRelay.tsx:37:16:37:40 | useClie ... ry, {}) | testReactRelay.tsx:38:49:38:52 | data | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:37:16:37:40 | useClie ... ry, {}) | user-provided value | +| testReactRelay.tsx:47:46:47:49 | data | testReactRelay.tsx:44:10:44:13 | data | testReactRelay.tsx:47:46:47:49 | data | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:44:10:44:13 | data | user-provided value | +| testReactRelay.tsx:71:49:71:52 | data | testReactRelay.tsx:62:5:62:8 | data | testReactRelay.tsx:71:49:71:52 | data | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:62:5:62:8 | data | user-provided value | +| testReactRelay.tsx:88:50:88:61 | feedbackText | testReactRelay.tsx:83:17:83:20 | data | testReactRelay.tsx:88:50:88:61 | feedbackText | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:83:17:83:20 | data | user-provided value | +| testReactRelay.tsx:113:48:113:58 | fragmentRef | testReactRelay.tsx:100:14:100:16 | res | testReactRelay.tsx:113:48:113:58 | fragmentRef | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:100:14:100:16 | res | user-provided value | +| testReactRelay.tsx:127:35:127:43 | data.user | testReactRelay.tsx:124:12:124:15 | data | testReactRelay.tsx:127:35:127:43 | data.user | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:124:12:124:15 | data | user-provided value | +| testReactRelay.tsx:137:50:137:53 | data | testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | testReactRelay.tsx:137:50:137:53 | data | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | user-provided value | edges | test.jsx:5:11:5:63 | response | test.jsx:6:24:6:31 | response | provenance | | | test.jsx:5:22:5:63 | await f ... ntent") | test.jsx:5:11:5:63 | response | provenance | | @@ -10,6 +20,31 @@ edges | test.jsx:6:24:6:38 | response.json() | test.jsx:6:18:6:38 | await r ... .json() | provenance | | | test.jsx:7:12:7:15 | data | test.jsx:15:11:17:5 | data | provenance | | | test.jsx:15:11:17:5 | data | test.jsx:27:29:27:32 | data | provenance | | +| testReactRelay.tsx:5:9:5:52 | commentData | testReactRelay.tsx:7:43:7:53 | commentData | provenance | | +| testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | testReactRelay.tsx:5:9:5:52 | commentData | provenance | | +| testReactRelay.tsx:7:43:7:53 | commentData | testReactRelay.tsx:7:43:7:58 | commentData.text | provenance | | +| testReactRelay.tsx:17:9:17:42 | data | testReactRelay.tsx:18:48:18:51 | data | provenance | | +| testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | testReactRelay.tsx:17:9:17:42 | data | provenance | | +| testReactRelay.tsx:18:48:18:51 | data | testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | provenance | | +| testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | provenance | | +| testReactRelay.tsx:37:9:37:40 | data | testReactRelay.tsx:38:49:38:52 | data | provenance | | +| testReactRelay.tsx:37:16:37:40 | useClie ... ry, {}) | testReactRelay.tsx:37:9:37:40 | data | provenance | | +| testReactRelay.tsx:44:9:44:70 | data | testReactRelay.tsx:47:46:47:49 | data | provenance | | +| testReactRelay.tsx:44:10:44:13 | data | testReactRelay.tsx:44:9:44:70 | data | provenance | | +| testReactRelay.tsx:61:9:70:38 | data | testReactRelay.tsx:71:49:71:52 | data | provenance | | +| testReactRelay.tsx:62:5:62:8 | data | testReactRelay.tsx:61:9:70:38 | data | provenance | | +| testReactRelay.tsx:80:9:80:54 | feedbackText | testReactRelay.tsx:88:50:88:61 | feedbackText | provenance | | +| testReactRelay.tsx:80:10:80:21 | feedbackText | testReactRelay.tsx:80:9:80:54 | feedbackText | provenance | | +| testReactRelay.tsx:83:17:83:20 | data | testReactRelay.tsx:84:23:84:26 | data | provenance | | +| testReactRelay.tsx:84:23:84:26 | data | testReactRelay.tsx:80:10:80:21 | feedbackText | provenance | | +| testReactRelay.tsx:95:9:95:50 | fragmentRef | testReactRelay.tsx:113:48:113:58 | fragmentRef | provenance | | +| testReactRelay.tsx:95:10:95:20 | fragmentRef | testReactRelay.tsx:95:9:95:50 | fragmentRef | provenance | | +| testReactRelay.tsx:100:14:100:16 | res | testReactRelay.tsx:101:22:101:24 | res | provenance | | +| testReactRelay.tsx:101:22:101:24 | res | testReactRelay.tsx:95:10:95:20 | fragmentRef | provenance | | +| testReactRelay.tsx:124:12:124:15 | data | testReactRelay.tsx:127:35:127:38 | data | provenance | | +| testReactRelay.tsx:127:35:127:38 | data | testReactRelay.tsx:127:35:127:43 | data.user | provenance | | +| testReactRelay.tsx:136:9:136:39 | data | testReactRelay.tsx:137:50:137:53 | data | provenance | | +| testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | testReactRelay.tsx:136:9:136:39 | data | provenance | | nodes | test.jsx:5:11:5:63 | response | semmle.label | response | | test.jsx:5:22:5:63 | await f ... ntent") | semmle.label | await f ... ntent") | @@ -21,4 +56,39 @@ nodes | test.jsx:7:12:7:15 | data | semmle.label | data | | test.jsx:15:11:17:5 | data | semmle.label | data | | test.jsx:27:29:27:32 | data | semmle.label | data | +| testReactRelay.tsx:5:9:5:52 | commentData | semmle.label | commentData | +| testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | semmle.label | useFrag ... entRef) | +| testReactRelay.tsx:7:43:7:53 | commentData | semmle.label | commentData | +| testReactRelay.tsx:7:43:7:58 | commentData.text | semmle.label | commentData.text | +| testReactRelay.tsx:17:9:17:42 | data | semmle.label | data | +| testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | semmle.label | useLazy ... ry, {}) | +| testReactRelay.tsx:18:48:18:51 | data | semmle.label | data | +| testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | semmle.label | data.co ... 0].text | +| testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | semmle.label | usePrel ... erence) | +| testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | semmle.label | usePrel ... r?.name | +| testReactRelay.tsx:37:9:37:40 | data | semmle.label | data | +| testReactRelay.tsx:37:16:37:40 | useClie ... ry, {}) | semmle.label | useClie ... ry, {}) | +| testReactRelay.tsx:38:49:38:52 | data | semmle.label | data | +| testReactRelay.tsx:44:9:44:70 | data | semmle.label | data | +| testReactRelay.tsx:44:10:44:13 | data | semmle.label | data | +| testReactRelay.tsx:47:46:47:49 | data | semmle.label | data | +| testReactRelay.tsx:61:9:70:38 | data | semmle.label | data | +| testReactRelay.tsx:62:5:62:8 | data | semmle.label | data | +| testReactRelay.tsx:71:49:71:52 | data | semmle.label | data | +| testReactRelay.tsx:80:9:80:54 | feedbackText | semmle.label | feedbackText | +| testReactRelay.tsx:80:10:80:21 | feedbackText | semmle.label | feedbackText | +| testReactRelay.tsx:83:17:83:20 | data | semmle.label | data | +| testReactRelay.tsx:84:23:84:26 | data | semmle.label | data | +| testReactRelay.tsx:88:50:88:61 | feedbackText | semmle.label | feedbackText | +| testReactRelay.tsx:95:9:95:50 | fragmentRef | semmle.label | fragmentRef | +| testReactRelay.tsx:95:10:95:20 | fragmentRef | semmle.label | fragmentRef | +| testReactRelay.tsx:100:14:100:16 | res | semmle.label | res | +| testReactRelay.tsx:101:22:101:24 | res | semmle.label | res | +| testReactRelay.tsx:113:48:113:58 | fragmentRef | semmle.label | fragmentRef | +| testReactRelay.tsx:124:12:124:15 | data | semmle.label | data | +| testReactRelay.tsx:127:35:127:38 | data | semmle.label | data | +| testReactRelay.tsx:127:35:127:43 | data.user | semmle.label | data.user | +| testReactRelay.tsx:136:9:136:39 | data | semmle.label | data | +| testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | semmle.label | readFra ... y, key) | +| testReactRelay.tsx:137:50:137:53 | data | semmle.label | data | subpaths diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactRelay.tsx b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactRelay.tsx new file mode 100644 index 000000000000..d40c01934070 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactRelay.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import { useFragment } from 'react-relay'; + +const func1 = ({ commentRef, query }) => { + const commentData = useFragment(query, commentRef); // $ Source=[js/xss] + return ( +

// $ Alert=[js/xss] + {" "} + {commentData.text} +

+ ); +}; + +import { useLazyLoadQuery } from "react-relay"; + +function func2({ query }) { + const data = useLazyLoadQuery(query, {}); // $ Source + return

; // $ Alert +} + +import { useQueryLoader, usePreloadedQuery } from "react-relay"; + +function func3({ initialQueryRef, query }) { + const [queryReference, loadQuery] = useQueryLoader(query, initialQueryRef); + return ( +

+ ); +} + +import { useClientQuery } from "react-relay"; + +function func4({ query }) { + const data = useClientQuery(query, {}); // $ Source + return

; // $ Alert +} + +import { useRefetchableFragment } from "react-relay"; + +function func5({ query, props }) { + const [data, refetch] = useRefetchableFragment(query, props.comment); // $ Source + return ( + <> +

// $ Alert +

+ + + ); +} + +import { usePaginationFragment } from "react-relay"; + +function func6({ query }) { + const { + data, // $ Source + loadNext, + loadPrevious, + hasNext, + hasPrevious, + isLoadingNext, + isLoadingPrevious, + refetch, + } = usePaginationFragment(query, {}); + return

; // $ Alert +} + + +import { useMutation } from 'react-relay'; +import type { FeedbackLikeMutation } from './FeedbackLikeMutation.graphql'; + +function func7(query) { + const [commit, inFlight] = useMutation(query); + const [feedbackText, setFeedbackText] = useState(''); + + commit({ + onCompleted(data) { // $ Source + setFeedbackText(data); + }, + }); + + return (
); // $ Alert +} + +import { useSubscription } from 'react-relay'; +import { useMemo } from 'react'; + +function func8({GroupLessonsSubscription}) { + const [fragmentRef, setFragmentRef] = useState(); + + const groupLessonConfig = useMemo(() => ({ + subscription: GroupLessonsSubscription, + variables: {}, + onNext: (res) => { // $ Source + setFragmentRef(res); + }, + onError: (err) => { + console.error('Error with subscription:', err); + }, + onCompleted: () => { + console.log('Subscription completed'); + }, + }), []); + + useSubscription(groupLessonConfig); + +return (
); // $ Alert +} + + +import { fetchQuery } from 'react-relay' + +function func9({query, environment}) { + fetchQuery(environment, query,{id: 4},).subscribe({ + start: () => {}, + complete: () => {}, + error: (error) => {}, + next: (data) => { // $ Source + const outputElement = document.getElementById('output'); + if (outputElement) { + outputElement.innerHTML = data.user; // $ Alert + } + } + }); +} + +import { readFragment } from "relay-runtime"; + +function func10({ query, key }) { + const data = readFragment(query, key); // $ Source + return (

); // $ Alert +} diff --git a/shared/mad/codeql/mad/ModelValidation.qll b/shared/mad/codeql/mad/ModelValidation.qll index 012e255580a0..4c1d6793d652 100644 --- a/shared/mad/codeql/mad/ModelValidation.qll +++ b/shared/mad/codeql/mad/ModelValidation.qll @@ -125,7 +125,7 @@ module KindValidation { // C# "file-write", "windows-registry", // JavaScript - "database-access-result" + "database-access-result", "response", "request" ] or this.matches([