mirror of https://github.com/mongodb/mongo
339 lines
12 KiB
JavaScript
339 lines
12 KiB
JavaScript
/**
|
|
* Tests how various aggregation expressions and stages that take strings and parameters respond to
|
|
* string input containing null bytes.
|
|
*/
|
|
|
|
const coll = db[jsTestName()];
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert({_id: 1, foo: 1}));
|
|
|
|
const nullByteStrs = [
|
|
// Starts with null chars.
|
|
"\x00a",
|
|
// Ends with null chars.
|
|
"a\x00",
|
|
// All null chars.
|
|
"\x00",
|
|
"\x00\x00\x00",
|
|
// Null chars somewhere in the middle.
|
|
"a\x00\x01\x08a",
|
|
"a\x00\x02\x08b",
|
|
"a\x00\x01\x18b",
|
|
"a\x00\x01\x28c",
|
|
"a\x00\x01\x03d\x00\xff\xff\xff\xff\x00\x08b",
|
|
];
|
|
|
|
function getStringUses(str) {
|
|
return [str, `foo.${str}`];
|
|
}
|
|
|
|
function getFieldUses(str) {
|
|
return [`$${str}`, `$foo.${str}`];
|
|
}
|
|
|
|
function getAllUses(str) {
|
|
return [...getStringUses(str), ...getFieldUses(str)];
|
|
}
|
|
|
|
// Confirm that the JavaScript engine in the shell fails to construct the JS object because
|
|
// 'JavaScript property (name) contains a null char which is not allowed in BSON'.
|
|
function getShellErrorPipelines(nullStr) {
|
|
return [
|
|
[{$documents: [{[nullStr]: "foo"}], $match: {}}],
|
|
[{$facet: {[nullStr]: [{$match: {}}]}}],
|
|
[{$fill: {output: {[nullStr]: {value: "foo"}}}}],
|
|
[{$fill: {sortBy: {[nullStr]: 1}, output: {[nullStr]: {value: "foo"}}}}],
|
|
[{$group: {_id: "$foo", [nullStr]: {$sum: "$bar"}}}],
|
|
[{$match: {[nullStr]: "foo"}}],
|
|
[{$match: {$or: [{"foo": "bar"}, {[nullStr]: "baz"}]}}],
|
|
[{
|
|
$match:
|
|
{$jsonSchema: {required: ["foo"], properties: {[nullStr]: {bsonType: "string"}}}}
|
|
}],
|
|
[{$merge: {into: "coll", on: "_id", let : {[nullStr]: "$foo"}}}],
|
|
[{$project: {[nullStr]: 1}}],
|
|
[{$project: {result: {$let: {vars: {[nullStr]: "$foo"}, in : "$$nullStr"}}}}],
|
|
[{$replaceRoot: {newRoot: {[nullStr]: "$foo"}}}],
|
|
[{$replaceWith: {[nullStr]: "$foo"}}],
|
|
[{$set: {[nullStr]: "$foo"}}],
|
|
[{$setWindowFields: {sortBy: {[nullStr]: 1}, output: {count: {$sum: 1}}}}],
|
|
[{$setWindowFields: {output: {[nullStr]: {count: {$sum: 1}}}}}],
|
|
[{$sort: {[nullStr]: 1}}],
|
|
[{$unset: {[nullStr]: ""}}]
|
|
];
|
|
}
|
|
|
|
for (const nullStr of nullByteStrs) {
|
|
for (const str of getAllUses(nullStr)) {
|
|
for (const pipeline of getShellErrorPipelines(str)) {
|
|
assert.throwsWithCode(() => coll.aggregate(pipeline), 16985);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Certain expressions and stages are valid when passed a literal string that contains a null byte,
|
|
// but are invalid when the string is a reference to a field name.
|
|
function getFieldPathErrorPipelines(nullStr) {
|
|
let pipelines = [
|
|
[{$addFields: {field: nullStr}}],
|
|
[{$addFields: {hashedVal: {$toHashedIndexKey: nullStr}}}],
|
|
[{$set: {field: nullStr}}],
|
|
[{$group: {_id: nullStr}}],
|
|
[{$bucket: {groupBy: "$foo", boundaries: [nullStr, nullStr + "1"], default: "Other"}}],
|
|
[{$bucket: {groupBy: "$foo", boundaries: [0, 5, 10], default: nullStr}}],
|
|
[{
|
|
$fill:
|
|
{partitionBy: {bar: nullStr}, sortBy: {foo: 1}, output: {out: {method: "linear"}}}
|
|
}],
|
|
[{$setWindowFields: {partitionBy: nullStr, output: {count: {$sum: 1}}}}],
|
|
];
|
|
|
|
const nullStrComparisons = [
|
|
{$eq: ["foo", nullStr]},
|
|
{$ne: ["foo", nullStr]},
|
|
{$gt: ["foo", nullStr]},
|
|
{$gte: ["foo", nullStr]},
|
|
{$lt: ["foo", nullStr]},
|
|
{$lte: ["foo", nullStr]},
|
|
{$in: ["foo", [nullStr]]}
|
|
];
|
|
|
|
pipelines =
|
|
pipelines.concat(nullStrComparisons.map(expr => [{$match: {$expr: {field: expr}}}]));
|
|
|
|
const expressionTests = [
|
|
{$concat: [nullStr, "foo"]},
|
|
{$ltrim: {input: nullStr}},
|
|
{$max: ["foo", nullStr]},
|
|
{$min: ["foo", nullStr]},
|
|
{$rtrim: {input: nullStr}},
|
|
{$substr: [nullStr, 0, 1]},
|
|
{$substrBytes: [nullStr, 0, 1]},
|
|
{$substrCP: [nullStr, 0, 1]},
|
|
{$strcasecmp: [nullStr, "foo"]},
|
|
{$trim: {input: nullStr}},
|
|
{$toLower: nullStr},
|
|
{$toString: nullStr},
|
|
{$toUpper: nullStr},
|
|
{$reduce: {input: [nullStr], initialValue: "", in : ""}},
|
|
{$reduce: {input: ["foo"], initialValue: nullStr, in : ""}},
|
|
{$reduce: {input: ["foo"], initialValue: "", in : nullStr}},
|
|
{$regexMatch: {input: nullStr, regex: "foo"}},
|
|
{$getField: nullStr},
|
|
];
|
|
|
|
return pipelines.concat(expressionTests.map(operator => [{$project: {field: operator}}]));
|
|
}
|
|
|
|
// Confirm the behavior for all the pipelines that should succeed with null-byte literal strings and
|
|
// fail with field path expressions containing a null byte.
|
|
for (const nullStr of nullByteStrs) {
|
|
for (const str of getStringUses(nullStr)) {
|
|
for (const pipeline of getFieldPathErrorPipelines(str)) {
|
|
assert.commandWorked(coll.runCommand('aggregate', {pipeline: pipeline, cursor: {}}));
|
|
}
|
|
}
|
|
// When there is an embedded null byte in a field path, we expect error code 16411 in
|
|
// particular.
|
|
for (const field of getFieldUses(nullStr)) {
|
|
for (const pipeline of getFieldPathErrorPipelines(field)) {
|
|
assert.throwsWithCode(() => coll.aggregate(pipeline), [16411, 9423101]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return expressions that should always fail when passed a string (literal or field name)
|
|
// containing a null byte.
|
|
function getErrorPipelines(nullStr) {
|
|
return [
|
|
{
|
|
pipeline: [{$bucket: {groupBy: nullStr, boundaries: [0, 5, 10], default: "Other"}}],
|
|
codes: [40202, 16411]
|
|
},
|
|
{
|
|
pipeline: [{$bucketAuto: {groupBy: nullStr, buckets: 5, output: {count: {$sum: 1}}}}],
|
|
codes: [40239, 16411]
|
|
},
|
|
{pipeline: [{$changeStream: {fullDocument: nullStr}}], codes: [ErrorCodes.BadValue]},
|
|
{
|
|
pipeline: [{$changeStream: {fullDocumentBeforeChange: nullStr}}],
|
|
codes: [ErrorCodes.BadValue]
|
|
},
|
|
{pipeline: [{$count: nullStr}], codes: [40159, 40158]},
|
|
{
|
|
pipeline: [{$densify: {field: nullStr, range: {step: 1, bounds: "full"}}}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{$densify: {field: "foo", range: {step: 1, bounds: nullStr}}}],
|
|
codes: [5946802]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$densify: {
|
|
field: "foo",
|
|
partitionByFields: [nullStr],
|
|
range: {step: 1, bounds: "full"}
|
|
}
|
|
}],
|
|
codes: [16411, 16410, 8993000]
|
|
},
|
|
{
|
|
pipeline: [{$fill: {partitionByFields: [nullStr], output: {foo: {value: "bar"}}}}],
|
|
codes: [9527900]
|
|
},
|
|
{
|
|
pipeline:
|
|
[{$geoNear: {near: {type: "Point", coordinates: [0, 0]}, distanceField: nullStr}}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$geoNear: {
|
|
near: {type: "Point", coordinates: [0, 0]},
|
|
distanceField: "foo",
|
|
includeLocs: nullStr
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$graphLookup: {
|
|
from: nullStr,
|
|
startWith: "$foo",
|
|
connectFromField: "parentId",
|
|
connectToField: "_id",
|
|
as: "results"
|
|
}
|
|
}],
|
|
codes: [ErrorCodes.InvalidNamespace]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$graphLookup: {
|
|
from: "coll",
|
|
startWith: "$foo",
|
|
connectFromField: nullStr,
|
|
connectToField: "_id",
|
|
as: "results"
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$graphLookup: {
|
|
from: "coll",
|
|
startWith: "$foo",
|
|
connectFromField: "parentId",
|
|
connectToField: nullStr,
|
|
as: "results"
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$graphLookup: {
|
|
from: "coll",
|
|
startWith: "$foo",
|
|
connectFromField: "parentId",
|
|
connectToField: "_id",
|
|
as: nullStr
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$graphLookup: {
|
|
from: "coll",
|
|
startWith: "$foo",
|
|
connectFromField: "parentId",
|
|
connectToField: "_id",
|
|
as: "results",
|
|
depthField: nullStr
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$lookup: {
|
|
from: nullStr,
|
|
localField: "local",
|
|
foreignField: "foreign",
|
|
as: "result"
|
|
}
|
|
}],
|
|
codes: [ErrorCodes.InvalidNamespace]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$lookup: {
|
|
from: "foo",
|
|
localField: nullStr,
|
|
foreignField: "foreign",
|
|
as: "result"
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$lookup: {
|
|
from: "foo",
|
|
localField: "local",
|
|
foreignField: nullStr,
|
|
as: "result"
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{
|
|
pipeline: [{
|
|
$lookup: {
|
|
from: "foo",
|
|
localField: "local",
|
|
foreignField: "foreign",
|
|
as: nullStr
|
|
}
|
|
}],
|
|
codes: [16411, 16410]
|
|
},
|
|
{pipeline: [{$merge: {into: nullStr}}], codes: [ErrorCodes.InvalidNamespace]},
|
|
{pipeline: [{$merge: {into: "coll", on: nullStr}}], codes: [16411, 16410]},
|
|
{pipeline: [{$out: {db: nullStr, coll: "coll"}}], codes: [ErrorCodes.InvalidNamespace]},
|
|
{pipeline: [{$out: {db: "db", coll: nullStr}}], codes: [ErrorCodes.InvalidNamespace]},
|
|
{
|
|
pipeline:
|
|
[{$project: {field: {$setField: {field: nullStr, input: {}, value: "newField"}}}}],
|
|
codes: [9534700, 16411]
|
|
},
|
|
{
|
|
pipeline: [{$project: {field: {$unsetField: {field: nullStr, input: {}}}}}],
|
|
codes: [9534700, 16411]
|
|
},
|
|
{
|
|
pipeline: [{$project: {matches: {$regexMatch: {input: "$foo", regex: nullStr}}}}],
|
|
codes: [51109, 16411]
|
|
},
|
|
{pipeline: [{$replaceRoot: {newRoot: nullStr}}], codes: [40228, 16411, 8105800]},
|
|
{pipeline: [{$replaceWith: nullStr}], codes: [40228, 16411, 8105800]},
|
|
{pipeline: [{$sortByCount: nullStr}], codes: [40148, 16411]},
|
|
{pipeline: [{$unionWith: {coll: nullStr}}], codes: [ErrorCodes.InvalidNamespace]},
|
|
{pipeline: [{$unwind: {path: nullStr}}], codes: [28818, 16419]},
|
|
{pipeline: [{$unwind: {path: "$foo", includeArrayIndex: nullStr}}], codes: [16411, 28822]},
|
|
];
|
|
}
|
|
|
|
// Confirm the "error pipelines" always throw an exception.
|
|
for (const nullStr of nullByteStrs) {
|
|
for (const strOrField of getAllUses(nullStr)) {
|
|
for (const {pipeline, codes} of getErrorPipelines(strOrField)) {
|
|
assert.throwsWithCode(() => coll.aggregate(pipeline), [...codes, 9423101]);
|
|
}
|
|
}
|
|
}
|