diff --git a/javascript/ql/lib/change-notes/2026-09-30-openapi-backend.md b/javascript/ql/lib/change-notes/2026-09-30-openapi-backend.md new file mode 100644 index 000000000000..027b4bc9f7d9 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-09-30-openapi-backend.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added support for the `openapi-backend` package. In particular, `context.request` in operation handlers, security handlers, lifecycle handlers and `validate` predicates is now seen as a source of untrusted user input. diff --git a/javascript/ql/lib/ext/openapi-backend.model.yml b/javascript/ql/lib/ext/openapi-backend.model.yml new file mode 100644 index 000000000000..af7bb54d09d4 --- /dev/null +++ b/javascript/ql/lib/ext/openapi-backend.model.yml @@ -0,0 +1,22 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: typeModel + data: + # OpenAPIBackend instances: `new OpenAPIBackend(...)` and `await api.init()` + - ["openapi-backend.OpenAPIBackend", "openapi-backend", "Member[OpenAPIBackend,default].Instance"] + - ["openapi-backend.OpenAPIBackend", "openapi-backend.OpenAPIBackend", "Member[init].ReturnValue.Awaited"] + # Handler functions registered in the constructor + - ["openapi-backend.Handler", "openapi-backend", "Member[OpenAPIBackend,default].Argument[0].Member[handlers,securityHandlers].AnyMember"] + - ["openapi-backend.Handler", "openapi-backend", "Member[OpenAPIBackend,default].Argument[0].Member[validate]"] + # Handler functions registered after construction + - ["openapi-backend.Handler", "openapi-backend.OpenAPIBackend", "Member[register,registerHandler,registerSecurityHandler].Argument[1]"] + - ["openapi-backend.Handler", "openapi-backend.OpenAPIBackend", "Member[register].Argument[0].AnyMember"] + # The handler context, including TypeScript-annotated `(c: Context) => ...` + - ["openapi-backend.Context", "openapi-backend.Handler", "Parameter[0]"] + + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + - ["openapi-backend.Context", "Member[request]", "remote"] diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected index 4a4fe13b3231..57c5a35f07aa 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected @@ -47,6 +47,16 @@ | mysql.js:8:9:11:47 | connect ... ds) {}) | | mysql.js:14:9:16:47 | connect ... ds) {}) | | mysql.js:19:9:20:48 | connect ... ds) {}) | +| openapi-backend-typed.ts:6:40:6:105 | db.quer ... s.id}`) | +| openapi-backend.js:10:20:10:83 | db.quer ... s.id}`) | +| openapi-backend.js:11:24:11:90 | db.quer ... ms.id]) | +| openapi-backend.js:14:20:14:97 | db.quer ... y']}'`) | +| openapi-backend.js:20:10:20:62 | db.quer ... ame}'`) | +| openapi-backend.js:24:21:24:96 | db.quer ... me}')`) | +| openapi-backend.js:28:3:28:74 | db.quer ... sid}'`) | +| openapi-backend.js:33:44:33:105 | db.quer ... s.id}`) | +| openapi-backend.js:36:41:36:112 | db.quer ... s.id}`) | +| openapi-backend.js:41:5:41:69 | db.quer ... th}')`) | | pg-promise-types.ts:8:5:8:22 | this.db.one(taint) | | pg-promise.js:9:3:9:15 | db.any(query) | | pg-promise.js:10:3:10:16 | db.many(query) | diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected index 17d280d38096..06d9558e7adf 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected @@ -115,6 +115,15 @@ | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | mongooseModelClient.js:12:22:12:29 | req.body | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | This query object depends on a $@. | mongooseModelClient.js:12:22:12:29 | req.body | user-provided value | | mysql.js:15:18:15:65 | 'SELECT ... + temp | mysql.js:6:16:6:31 | req.params.value | mysql.js:15:18:15:65 | 'SELECT ... + temp | This query string depends on a $@. | mysql.js:6:16:6:31 | req.params.value | user-provided value | | mysql.js:19:26:19:73 | 'SELECT ... + temp | mysql.js:6:16:6:31 | req.params.value | mysql.js:19:26:19:73 | 'SELECT ... + temp | This query string depends on a $@. | mysql.js:6:16:6:31 | req.params.value | user-provided value | +| openapi-backend-typed.ts:6:49:6:104 | `SELECT ... ms.id}` | openapi-backend-typed.ts:6:84:6:92 | c.request | openapi-backend-typed.ts:6:49:6:104 | `SELECT ... ms.id}` | This query string depends on a $@. | openapi-backend-typed.ts:6:84:6:92 | c.request | user-provided value | +| openapi-backend.js:10:29:10:82 | `SELECT ... ms.id}` | openapi-backend.js:10:62:10:70 | c.request | openapi-backend.js:10:29:10:82 | `SELECT ... ms.id}` | This query string depends on a $@. | openapi-backend.js:10:62:10:70 | c.request | user-provided value | +| openapi-backend.js:14:29:14:96 | `SELECT ... ey']}'` | openapi-backend.js:14:64:14:72 | c.request | openapi-backend.js:14:29:14:96 | `SELECT ... ey']}'` | This query string depends on a $@. | openapi-backend.js:14:64:14:72 | c.request | user-provided value | +| openapi-backend.js:20:19:20:61 | `SELECT ... name}'` | openapi-backend.js:19:20:19:28 | c.request | openapi-backend.js:20:19:20:61 | `SELECT ... name}'` | This query string depends on a $@. | openapi-backend.js:19:20:19:28 | c.request | user-provided value | +| openapi-backend.js:24:30:24:95 | `INSERT ... ame}')` | openapi-backend.js:24:66:24:74 | c.request | openapi-backend.js:24:30:24:95 | `INSERT ... ame}')` | This query string depends on a $@. | openapi-backend.js:24:66:24:74 | c.request | user-provided value | +| openapi-backend.js:28:12:28:73 | `SELECT ... .sid}'` | openapi-backend.js:28:50:28:58 | c.request | openapi-backend.js:28:12:28:73 | `SELECT ... .sid}'` | This query string depends on a $@. | openapi-backend.js:28:50:28:58 | c.request | user-provided value | +| openapi-backend.js:33:53:33:104 | `DELETE ... ms.id}` | openapi-backend.js:33:84:33:92 | c.request | openapi-backend.js:33:53:33:104 | `DELETE ... ms.id}` | This query string depends on a $@. | openapi-backend.js:33:84:33:92 | c.request | user-provided value | +| openapi-backend.js:36:50:36:111 | `UPDATE ... ms.id}` | openapi-backend.js:36:91:36:99 | c.request | openapi-backend.js:36:50:36:111 | `UPDATE ... ms.id}` | This query string depends on a $@. | openapi-backend.js:36:91:36:99 | c.request | user-provided value | +| openapi-backend.js:41:14:41:68 | `INSERT ... ath}')` | openapi-backend.js:41:51:41:59 | c.request | openapi-backend.js:41:14:41:68 | `INSERT ... ath}')` | This query string depends on a $@. | openapi-backend.js:41:51:41:59 | c.request | user-provided value | | pg-promise-types.ts:8:17:8:21 | taint | pg-promise-types.ts:7:17:7:28 | req.params.x | pg-promise-types.ts:8:17:8:21 | taint | This query string depends on a $@. | pg-promise-types.ts:7:17:7:28 | req.params.x | user-provided value | | pg-promise.js:9:10:9:14 | query | pg-promise.js:7:16:7:34 | req.params.category | pg-promise.js:9:10:9:14 | query | This query string depends on a $@. | pg-promise.js:7:16:7:34 | req.params.category | user-provided value | | pg-promise.js:10:11:10:15 | query | pg-promise.js:7:16:7:34 | req.params.category | pg-promise.js:10:11:10:15 | query | This query string depends on a $@. | pg-promise.js:7:16:7:34 | req.params.category | user-provided value | @@ -549,6 +558,18 @@ edges | mysql.js:6:16:6:31 | req.params.value | mysql.js:6:9:6:12 | temp | provenance | | | mysql.js:15:62:15:65 | temp | mysql.js:15:18:15:65 | 'SELECT ... + temp | provenance | | | mysql.js:19:70:19:73 | temp | mysql.js:19:26:19:73 | 'SELECT ... + temp | provenance | | +| openapi-backend-typed.ts:6:84:6:92 | c.request | openapi-backend-typed.ts:6:49:6:104 | `SELECT ... ms.id}` | provenance | | +| openapi-backend.js:10:62:10:70 | c.request | openapi-backend.js:10:29:10:82 | `SELECT ... ms.id}` | provenance | | +| openapi-backend.js:14:64:14:72 | c.request | openapi-backend.js:14:29:14:96 | `SELECT ... ey']}'` | provenance | | +| openapi-backend.js:19:9:19:16 | { name } | openapi-backend.js:19:11:19:14 | name | provenance | | +| openapi-backend.js:19:11:19:14 | name | openapi-backend.js:20:55:20:58 | name | provenance | | +| openapi-backend.js:19:20:19:28 | c.request | openapi-backend.js:19:9:19:16 | { name } | provenance | | +| openapi-backend.js:20:55:20:58 | name | openapi-backend.js:20:19:20:61 | `SELECT ... name}'` | provenance | | +| openapi-backend.js:24:66:24:74 | c.request | openapi-backend.js:24:30:24:95 | `INSERT ... ame}')` | provenance | | +| openapi-backend.js:28:50:28:58 | c.request | openapi-backend.js:28:12:28:73 | `SELECT ... .sid}'` | provenance | | +| openapi-backend.js:33:84:33:92 | c.request | openapi-backend.js:33:53:33:104 | `DELETE ... ms.id}` | provenance | | +| openapi-backend.js:36:91:36:99 | c.request | openapi-backend.js:36:50:36:111 | `UPDATE ... ms.id}` | provenance | | +| openapi-backend.js:41:51:41:59 | c.request | openapi-backend.js:41:14:41:68 | `INSERT ... ath}')` | provenance | | | pg-promise-types.ts:7:9:7:13 | taint | pg-promise-types.ts:8:17:8:21 | taint | provenance | | | pg-promise-types.ts:7:17:7:28 | req.params.x | pg-promise-types.ts:7:9:7:13 | taint | provenance | | | pg-promise.js:6:7:6:11 | query | pg-promise.js:9:10:9:14 | query | provenance | | @@ -958,6 +979,27 @@ nodes | mysql.js:15:62:15:65 | temp | semmle.label | temp | | mysql.js:19:26:19:73 | 'SELECT ... + temp | semmle.label | 'SELECT ... + temp | | mysql.js:19:70:19:73 | temp | semmle.label | temp | +| openapi-backend-typed.ts:6:49:6:104 | `SELECT ... ms.id}` | semmle.label | `SELECT ... ms.id}` | +| openapi-backend-typed.ts:6:84:6:92 | c.request | semmle.label | c.request | +| openapi-backend.js:10:29:10:82 | `SELECT ... ms.id}` | semmle.label | `SELECT ... ms.id}` | +| openapi-backend.js:10:62:10:70 | c.request | semmle.label | c.request | +| openapi-backend.js:14:29:14:96 | `SELECT ... ey']}'` | semmle.label | `SELECT ... ey']}'` | +| openapi-backend.js:14:64:14:72 | c.request | semmle.label | c.request | +| openapi-backend.js:19:9:19:16 | { name } | semmle.label | { name } | +| openapi-backend.js:19:11:19:14 | name | semmle.label | name | +| openapi-backend.js:19:20:19:28 | c.request | semmle.label | c.request | +| openapi-backend.js:20:19:20:61 | `SELECT ... name}'` | semmle.label | `SELECT ... name}'` | +| openapi-backend.js:20:55:20:58 | name | semmle.label | name | +| openapi-backend.js:24:30:24:95 | `INSERT ... ame}')` | semmle.label | `INSERT ... ame}')` | +| openapi-backend.js:24:66:24:74 | c.request | semmle.label | c.request | +| openapi-backend.js:28:12:28:73 | `SELECT ... .sid}'` | semmle.label | `SELECT ... .sid}'` | +| openapi-backend.js:28:50:28:58 | c.request | semmle.label | c.request | +| openapi-backend.js:33:53:33:104 | `DELETE ... ms.id}` | semmle.label | `DELETE ... ms.id}` | +| openapi-backend.js:33:84:33:92 | c.request | semmle.label | c.request | +| openapi-backend.js:36:50:36:111 | `UPDATE ... ms.id}` | semmle.label | `UPDATE ... ms.id}` | +| openapi-backend.js:36:91:36:99 | c.request | semmle.label | c.request | +| openapi-backend.js:41:14:41:68 | `INSERT ... ath}')` | semmle.label | `INSERT ... ath}')` | +| openapi-backend.js:41:51:41:59 | c.request | semmle.label | c.request | | pg-promise-types.ts:7:9:7:13 | taint | semmle.label | taint | | pg-promise-types.ts:7:17:7:28 | req.params.x | semmle.label | req.params.x | | pg-promise-types.ts:8:17:8:21 | taint | semmle.label | taint | diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend-typed.ts b/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend-typed.ts new file mode 100644 index 000000000000..7db6e4d5b7b8 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend-typed.ts @@ -0,0 +1,8 @@ +import OpenAPIBackend, { Context } from 'openapi-backend'; +import { Pool } from 'pg'; + +const db = new Pool(); + +const getOwner = async (c: Context) => db.query(`SELECT * FROM owners WHERE id = ${c.request.params.id}`); // $ Alert + +export const api = new OpenAPIBackend({ definition: './openapi.yml', handlers: { getOwner } }); diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend.js b/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend.js new file mode 100644 index 000000000000..d6136b537c3b --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/openapi-backend.js @@ -0,0 +1,44 @@ +const { OpenAPIBackend } = require('openapi-backend'); +const OpenAPIBackendDefault = require('openapi-backend').default; +const { Pool } = require('pg'); + +const db = new Pool(); + +const api = new OpenAPIBackend({ + definition: './openapi.yml', + handlers: { + getPet: (c) => db.query(`SELECT * FROM pets WHERE id = ${c.request.params.id}`), // $ Alert + getPetSafe: (c) => db.query('SELECT * FROM pets WHERE id = $1', [c.request.params.id]), + }, + securityHandlers: { + apiKey: (c) => db.query(`SELECT * FROM keys WHERE key = '${c.request.headers['x-api-key']}'`), // $ Alert + }, +}); + +api.register('findPets', async (c) => { + const { name } = c.request.query; // $ Source + return db.query(`SELECT * FROM pets WHERE name = '${name}'`); // $ Alert +}); + +api.register({ + createPet: (c) => db.query(`INSERT INTO pets (name) VALUES ('${c.request.requestBody.name}')`), // $ Alert +}); + +api.registerSecurityHandler('session', (c) => + db.query(`SELECT * FROM sessions WHERE id = '${c.request.cookies.sid}'`), // $ Alert +); + +async function main() { + const initialized = await new OpenAPIBackendDefault({ definition: './openapi.yml' }).init(); + initialized.register('deletePet', (c) => db.query(`DELETE FROM pets WHERE id = ${c.request.params.id}`)); // $ Alert +} + +api.registerHandler('updatePet', (c) => db.query(`UPDATE pets SET name = 'x' WHERE id = ${c.request.params.id}`)); // $ Alert + +const audited = new OpenAPIBackend({ + definition: './openapi.yml', + validate: (c) => { + db.query(`INSERT INTO audit (path) VALUES ('${c.request.path}')`); // $ Alert + return true; + }, +});