Skip to content

Commit 30868d6

Browse files
committed
🔒 Fixed SQL injection in Content API slug filter ordering
ref https://linear.app/ghost/issue/ONC-1475 Switched to parameterized query bindings in the slug filter ordering logic so user input is never interpolated into SQL strings. Updated the crud plugin to thread bindings through when autoOrder returns a {sql, bindings} object, and bumped @tryghost/bookshelf-plugins to 0.6.29 which adds parameterized orderByRaw support. Reported-by: Nicholas Carlini <nicholas@carlini.com>
1 parent ebf4bb7 commit 30868d6

File tree

6 files changed

+125
-73
lines changed

6 files changed

+125
-73
lines changed

ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ const slugFilterOrder = (table, filter) => {
33

44
if (orderMatch) {
55
let orderSlugs = orderMatch[1].split(',');
6-
let order = 'CASE ';
6+
let caseParts = [];
7+
let bindings = [];
78

89
orderSlugs.forEach((slug, index) => {
9-
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
10+
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
11+
bindings.push(slug.trim(), index);
1012
});
1113

12-
order += 'END ASC';
13-
14-
return order;
14+
return {
15+
sql: `CASE ${caseParts.join(' ')} END ASC`,
16+
bindings
17+
};
1518
}
1619
};
1720

ghost/core/core/server/models/base/plugins/crud.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ module.exports = function (Bookshelf) {
101101
options.order = order;
102102
options.eagerLoad = eagerLoad;
103103
} else if (options.autoOrder) {
104-
options.orderRaw = options.autoOrder;
104+
if (typeof options.autoOrder === 'object') {
105+
options.orderRaw = options.autoOrder.sql;
106+
options.orderRawBindings = options.autoOrder.bindings;
107+
} else {
108+
options.orderRaw = options.autoOrder;
109+
}
105110
} else if (this.orderDefaultRaw) {
106111
options.orderRaw = this.orderDefaultRaw(options);
107112
} else if (this.orderDefaultOptions) {

ghost/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"@tryghost/adapter-base-cache": "0.1.17",
7373
"@tryghost/admin-api-schema": "4.5.10",
7474
"@tryghost/api-framework": "1.0.3",
75-
"@tryghost/bookshelf-plugins": "0.6.27",
75+
"@tryghost/bookshelf-plugins": "0.6.29",
7676
"@tryghost/color-utils": "0.2.10",
7777
"@tryghost/config-url-helpers": "1.0.17",
7878
"@tryghost/custom-fonts": "1.0.2",

ghost/core/test/e2e-api/content/tags.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ describe('Tags Content API', function () {
126126
assert(getTag('chorizo').url.endsWith('/tag/chorizo/'));
127127
});
128128

129+
it('Can request tags with slug filter ordering', async function () {
130+
const res = await request.get(localUtils.API.getApiQuery(`tags/?key=${validKey}&filter=slug:[bacon,chorizo]`))
131+
.set('Origin', testUtils.API.getURL())
132+
.expect('Content-Type', /json/)
133+
.expect('Cache-Control', testUtils.cacheRules.public)
134+
.expect(200);
135+
136+
const jsonResponse = res.body;
137+
assertExists(jsonResponse.tags);
138+
// Should return tags matching the slug filter, ordered by slug position
139+
assert.equal(jsonResponse.tags[0].slug, 'bacon');
140+
assert.equal(jsonResponse.tags[1].slug, 'chorizo');
141+
});
142+
129143
it('Can use single url field and have valid url fields', async function () {
130144
const res = await request.get(localUtils.API.getApiQuery(`tags/?key=${validKey}&fields=url`))
131145
.set('Origin', testUtils.API.getURL())
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const assert = require('node:assert/strict');
2+
const slugFilterOrder = require('../../../../../../../../core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order');
3+
4+
describe('Unit: endpoints/utils/serializers/input/utils/slug-filter-order', function () {
5+
it('returns parameterized sql and bindings for slug filter', function () {
6+
const result = slugFilterOrder('tags', 'slug:[kitchen-sink,bacon,chorizo]');
7+
8+
assert.equal(result.sql, 'CASE WHEN `tags`.`slug` = ? THEN ? WHEN `tags`.`slug` = ? THEN ? WHEN `tags`.`slug` = ? THEN ? END ASC');
9+
assert.deepEqual(result.bindings, ['kitchen-sink', 0, 'bacon', 1, 'chorizo', 2]);
10+
});
11+
12+
it('returns undefined when filter has no slug array', function () {
13+
const result = slugFilterOrder('tags', 'status:published');
14+
15+
assert.equal(result, undefined);
16+
});
17+
18+
it('trims whitespace from slug values', function () {
19+
const result = slugFilterOrder('posts', 'slug:[ foo , bar ]');
20+
21+
assert.deepEqual(result.bindings, ['foo', 0, 'bar', 1]);
22+
});
23+
24+
it('handles single slug', function () {
25+
const result = slugFilterOrder('posts', 'slug:[only-one]');
26+
27+
assert.equal(result.sql, 'CASE WHEN `posts`.`slug` = ? THEN ? END ASC');
28+
assert.deepEqual(result.bindings, ['only-one', 0]);
29+
});
30+
});

yarn.lock

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8699,95 +8699,95 @@
86998699
json-stable-stringify "1.3.0"
87008700
lodash "4.17.23"
87018701

8702-
"@tryghost/bookshelf-collision@^0.1.48":
8703-
version "0.1.48"
8704-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-collision/-/bookshelf-collision-0.1.48.tgz#3490c7ac46959e43571b402f3b75aa3b1e7d608b"
8705-
integrity sha512-JaOVHE8JJ3aVzCba55erGxNqyC9MHUnJhxPgn/XOb64JYcfU1tR+5VvPfxvQTBy9bIjhmVeedlp1CWdWzpCUxw==
8702+
"@tryghost/bookshelf-collision@^0.1.49":
8703+
version "0.1.49"
8704+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-collision/-/bookshelf-collision-0.1.49.tgz#f3e15954446d89ae867e40a1bb488be1d7c65d38"
8705+
integrity sha512-WW2+Uk6h7nEL5cW2BOaoDMLZeWuur+PEXV726JpH4vQK9D7UQcHwRQW7CBu+NW6tMbjWkhPPp4q0lBKdoosusQ==
87068706
dependencies:
8707-
"@tryghost/errors" "^1.3.8"
8707+
"@tryghost/errors" "^1.3.9"
87088708
lodash "^4.17.21"
87098709
moment-timezone "^0.5.33"
87108710

8711-
"@tryghost/bookshelf-custom-query@^0.1.30":
8712-
version "0.1.30"
8713-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-custom-query/-/bookshelf-custom-query-0.1.30.tgz#178cd56db9a86ae20bef3029bdfd229c3f7b7ba2"
8714-
integrity sha512-OTExkXpRZ26JXMBNUV9A0MNtQpx3wZvohaAKk/hA8NtjwjX82IXOfG/L78Dex/iolewBZhSKjpC33vnFKvChmA==
8711+
"@tryghost/bookshelf-custom-query@^0.1.31":
8712+
version "0.1.31"
8713+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-custom-query/-/bookshelf-custom-query-0.1.31.tgz#9bea3f73168224a42f2bf312ce3e939977ed2085"
8714+
integrity sha512-HZs/WnNQpKLG+nkBSbe+wsi1WP+jq56wiIZCnbWTfX3xKFqLh+QRtkmgvJCpyd9iEgexWzodmyIZY3th3sv6wA==
87158715

8716-
"@tryghost/bookshelf-eager-load@^0.1.34":
8717-
version "0.1.34"
8718-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-eager-load/-/bookshelf-eager-load-0.1.34.tgz#894756cc8a5d7f4d5ef89717947adaddcd6e20c9"
8719-
integrity sha512-u7DSnnA+2MBc27B/EiyHx2t2tSQPFCsUHv45A95r/ETRvERZUySb5fKAb9PJlmvpD+qQ3SV7EsW3cERhIBUU6A==
8716+
"@tryghost/bookshelf-eager-load@^0.1.35":
8717+
version "0.1.35"
8718+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-eager-load/-/bookshelf-eager-load-0.1.35.tgz#b04e21e94ae9b24fde35194662f8f5d412f2c06a"
8719+
integrity sha512-8BkBxnETuoVoK48rlCdlCL0upTWwc1Ku21Wjfafor6kJDOO9DxsLDi/e0fNQxMTqgH0aWdWYDeyNAjHJgiNJJQ==
87208720
dependencies:
8721-
"@tryghost/debug" "^0.1.35"
8721+
"@tryghost/debug" "^0.1.36"
87228722
lodash "^4.17.21"
87238723

8724-
"@tryghost/bookshelf-filter@^0.5.23":
8725-
version "0.5.23"
8726-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-filter/-/bookshelf-filter-0.5.23.tgz#95a77343cb19c4be2e25069464b17f45b2ad19d6"
8727-
integrity sha512-8h+qFmNBajv86Ai2JhFaeDY7pPYeMjzbhDEuZBuAlFKhVD/2c+qvSGVXuDHAua2BXmArDKlvSO+VE9tMrhn94A==
8724+
"@tryghost/bookshelf-filter@^0.5.24":
8725+
version "0.5.24"
8726+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-filter/-/bookshelf-filter-0.5.24.tgz#13609c3c98f825bfbe713b7ee1f5ac751c18b498"
8727+
integrity sha512-ysPM3pVj4gFbX95Ba+fAkujCvM9/uSI61Hc2XiOLhCjOYCeETNOg1yuIYp/P72dRRnh9ibOBXhuXo71jxzrN4g==
87288728
dependencies:
8729-
"@tryghost/debug" "^0.1.35"
8730-
"@tryghost/errors" "^1.3.8"
8729+
"@tryghost/debug" "^0.1.36"
8730+
"@tryghost/errors" "^1.3.9"
87318731
"@tryghost/nql" "0.12.6"
8732-
"@tryghost/tpl" "^0.1.35"
8732+
"@tryghost/tpl" "^0.1.36"
87338733

8734-
"@tryghost/bookshelf-has-posts@^0.1.35":
8735-
version "0.1.35"
8736-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-has-posts/-/bookshelf-has-posts-0.1.35.tgz#21723c75540cf89851a311dbace55cff9eb996f3"
8737-
integrity sha512-Q+XHTIjeAV0gaeqEw1v+45Z25eWuSpy3wqRriKscmdciZ9pDj5j9erQQ+aQHrz0XirqiN5J+/bZIjPCfBeOvVQ==
8734+
"@tryghost/bookshelf-has-posts@^0.1.36":
8735+
version "0.1.36"
8736+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-has-posts/-/bookshelf-has-posts-0.1.36.tgz#795eaacfb31cf48a08b41537d0b8035bc54f3833"
8737+
integrity sha512-qUhy2nqJVlmBXkCihHuxVALf/CGKVRxwFb2GIH5aAJVmET/HyfIRG+5lutbbJvQX7ns2RqQ325azgWSDnUvmoA==
87388738
dependencies:
8739-
"@tryghost/debug" "^0.1.35"
8739+
"@tryghost/debug" "^0.1.36"
87408740
lodash "^4.17.21"
87418741

8742-
"@tryghost/bookshelf-include-count@^0.3.18":
8743-
version "0.3.18"
8744-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-include-count/-/bookshelf-include-count-0.3.18.tgz#bae6971eeb3ec26867fe6bca639fcb7478bb7c3a"
8745-
integrity sha512-DJjvcPSmGhm4y2GFb47WcIf+CHRz+nGbPJPqcNwWsi16hdDqMNhhB9VLqRlhSTvW+KrdAhnvy4+LL8x8jtHeDg==
8742+
"@tryghost/bookshelf-include-count@^0.3.19":
8743+
version "0.3.19"
8744+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-include-count/-/bookshelf-include-count-0.3.19.tgz#dc8b9a9b244ca71c22f58945acb55f4c0333a5de"
8745+
integrity sha512-2iD8bxB+8wr3NpGz/WzoJun7c6eJDPXWGfduYOy/SPl/XUroQTuSbdmUKhks2H9Y6fy++QWf34HN51930YldTw==
87468746
dependencies:
8747-
"@tryghost/debug" "^0.1.35"
8747+
"@tryghost/debug" "^0.1.36"
87488748
lodash "^4.17.21"
87498749

8750-
"@tryghost/bookshelf-order@^0.1.30":
8751-
version "0.1.30"
8752-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-order/-/bookshelf-order-0.1.30.tgz#1347f174a79d91c2fa83535adaead45782312337"
8753-
integrity sha512-QJRJBAPm7/mQaB46Jr+Tuhz6eEPAryhL4OjI2jVjLLMwYBYMaJEJTfmc6BaPvHhF3zaPD8s6K4J6YVUvh59mCg==
8750+
"@tryghost/bookshelf-order@^0.1.31":
8751+
version "0.1.31"
8752+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-order/-/bookshelf-order-0.1.31.tgz#86cf18ee36a24231f866e6b02211701fdd89eb4d"
8753+
integrity sha512-6Nyj+nB966EFtwEpL/ibrmKFXj6ms32LUdpOMWbe2IM9IF9whDoFFUiYDW2g6rTzsU2B4hfLhyQQlMzmEAI/HA==
87548754
dependencies:
87558755
lodash "^4.17.21"
87568756

8757-
"@tryghost/bookshelf-pagination@^0.1.51":
8758-
version "0.1.51"
8759-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-pagination/-/bookshelf-pagination-0.1.51.tgz#0d048ede46a16add7b02fc2e7ae52a894920ddba"
8760-
integrity sha512-koHwelNrhTY0bHtKLTFNhqQlf4j5vUAMEJBoNFLd3SwhK9RV/3f3xmo+N1HVB1NyadwA4DUBXkzdjFWoo/0T4g==
8757+
"@tryghost/bookshelf-pagination@^0.1.53":
8758+
version "0.1.53"
8759+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-pagination/-/bookshelf-pagination-0.1.53.tgz#e046b4889aa07ac81136346dcf6bb0fdc516f261"
8760+
integrity sha512-admXVnNJpiJy9rPx3oAlbKtF+sNCJMKj4wPhcACmBhv+HWvuGQcCECZJuuHOecEV3RuB8Y5iIC72QiG5MUNUZg==
87618761
dependencies:
8762-
"@tryghost/errors" "^1.3.8"
8763-
"@tryghost/tpl" "^0.1.35"
8762+
"@tryghost/errors" "^1.3.9"
8763+
"@tryghost/tpl" "^0.1.36"
87648764
lodash "^4.17.21"
87658765

8766-
"@tryghost/bookshelf-plugins@0.6.27":
8767-
version "0.6.27"
8768-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-plugins/-/bookshelf-plugins-0.6.27.tgz#830b225bae1d3690efe73d589ac0cdf719866ac9"
8769-
integrity sha512-pCcLWl9L7HfP6LTLsy3o84ydjyJcfpSNILLtpbEXbkOulxneVMY5+3ffem6dXBNBPCEisyw3/AIlmrznsFK36Q==
8770-
dependencies:
8771-
"@tryghost/bookshelf-collision" "^0.1.48"
8772-
"@tryghost/bookshelf-custom-query" "^0.1.30"
8773-
"@tryghost/bookshelf-eager-load" "^0.1.34"
8774-
"@tryghost/bookshelf-filter" "^0.5.23"
8775-
"@tryghost/bookshelf-has-posts" "^0.1.35"
8776-
"@tryghost/bookshelf-include-count" "^0.3.18"
8777-
"@tryghost/bookshelf-order" "^0.1.30"
8778-
"@tryghost/bookshelf-pagination" "^0.1.51"
8779-
"@tryghost/bookshelf-search" "^0.1.30"
8780-
"@tryghost/bookshelf-transaction-events" "^0.2.19"
8781-
8782-
"@tryghost/bookshelf-search@^0.1.30":
8783-
version "0.1.30"
8784-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-search/-/bookshelf-search-0.1.30.tgz#e886668be8aea35b95f053c5acd0bda3cb6db2e3"
8785-
integrity sha512-NjxdrXfmHJClyNBYQJ4tXKsQBRZvIx/wJ0WWJ1/bn0iTDykBZweMOTdtzrwg9RRdct/FyaYDFY+tvo5cRslepA==
8786-
8787-
"@tryghost/bookshelf-transaction-events@^0.2.19":
8788-
version "0.2.19"
8789-
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-transaction-events/-/bookshelf-transaction-events-0.2.19.tgz#96b17982ec61ec795c83486b5b3dcc347d9eaca5"
8790-
integrity sha512-RSuckgQ2EC2wNt78Qpfc5f8qjCea5lgiEpgN1Gb+PYw3Erhd1dSPKxqztF5ktU1WeWXYXgx0C8QNf1hGfPb2qw==
8766+
"@tryghost/bookshelf-plugins@0.6.29":
8767+
version "0.6.29"
8768+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-plugins/-/bookshelf-plugins-0.6.29.tgz#920d129d68adf41f0bfa97ab9a9f63f3b25f386e"
8769+
integrity sha512-vwQoMOZZgL2GF607/e6Fad4p0UuAY4UFP7XTUxGSt4PPVLZZ51UvqiC8h0QqjPOb6DmksKtrO6b8iKRtSaLEzA==
8770+
dependencies:
8771+
"@tryghost/bookshelf-collision" "^0.1.49"
8772+
"@tryghost/bookshelf-custom-query" "^0.1.31"
8773+
"@tryghost/bookshelf-eager-load" "^0.1.35"
8774+
"@tryghost/bookshelf-filter" "^0.5.24"
8775+
"@tryghost/bookshelf-has-posts" "^0.1.36"
8776+
"@tryghost/bookshelf-include-count" "^0.3.19"
8777+
"@tryghost/bookshelf-order" "^0.1.31"
8778+
"@tryghost/bookshelf-pagination" "^0.1.53"
8779+
"@tryghost/bookshelf-search" "^0.1.31"
8780+
"@tryghost/bookshelf-transaction-events" "^0.2.20"
8781+
8782+
"@tryghost/bookshelf-search@^0.1.31":
8783+
version "0.1.31"
8784+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-search/-/bookshelf-search-0.1.31.tgz#8627035a19bb3f21f5b4e167b4d1b6292a30e7fe"
8785+
integrity sha512-6/TgNXHP4YGGa481q5ikGpv4cBd+Ott7aq358yO80mvcVtp1VWiX9R4dn9HRj/EDqJmVJ9SArPgD8p0QgJlr/A==
8786+
8787+
"@tryghost/bookshelf-transaction-events@^0.2.20":
8788+
version "0.2.20"
8789+
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-transaction-events/-/bookshelf-transaction-events-0.2.20.tgz#0cabab289ff7773e2eb94a054cc4935f7ff3a13d"
8790+
integrity sha512-I/aiR64Rk+J78459qbGw2XnPN1f2PoUW21mEe+WHPLDV5sNzqSReI4kDW8s+text2cTLbcHjokQCOaG4l4HIug==
87918791

87928792
"@tryghost/bunyan-rotating-filestream@^0.0.7":
87938793
version "0.0.7"

0 commit comments

Comments
 (0)