Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

odata: handle invalid skiptokens #1278

Merged
merged 8 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/formats/odata.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,9 @@ const singleRowToOData = (fields, row, domain, originalUrl, query) => {
let { offset } = paging;

if (skipToken) {
offset = filtered.findIndex(s => skipToken.repeatId === s.__id) + 1;
const { repeatId } = skipToken;
if (!repeatId) throw Problem.user.odataInvalidSkiptoken();
offset = filtered.findIndex(s => repeatId === s.__id) + 1;
if (offset === 0) throw Problem.user.odataRepeatIdNotFound();
}

Expand Down
11 changes: 9 additions & 2 deletions lib/util/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,15 @@ class QueryOptions {
//
// See: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html
static parseSkiptoken(token) {
const jsonString = base64ToUtf8(token.substr(2));
return JSON.parse(jsonString);
if (!token.startsWith('01')) throw Problem.user.odataInvalidSkiptoken();

try {
const parsed = JSON.parse(base64ToUtf8(token.substr(2)));
if (typeof parsed !== 'object') throw Problem.user.odataInvalidSkiptoken();
return parsed;
} catch (err) {
throw Problem.user.odataInvalidSkiptoken();
}
}

static getSkiptoken(data) {
Expand Down
2 changes: 2 additions & 0 deletions lib/util/problem.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ const problems = {

odataRepeatIdNotFound: problem(400.34, () => 'Record associated with the provided $skiptoken not found.'),

odataInvalidSkiptoken: problem(400.35, () => 'Invalid $skiptoken'),

// no detail information for security reasons.
authenticationFailed: problem(401.2, () => 'Could not authenticate with the provided credentials.'),

Expand Down
34 changes: 34 additions & 0 deletions test/unit/formats/odata.js
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,40 @@ describe('odata message composition', () => {

const nomatch = '0000000000000000000000000000000000000000';

const stringify64 = obj => Buffer.from(JSON.stringify(obj)).toString('base64');

[
'nonsense',

// no version + valid token
stringify64({ repeatId: billy.__id }),

// incorrect version number + valid token
'00' + stringify64({ repeatId: billy.__id }),
'02' + stringify64({ repeatId: billy.__id }),

// correct version plus non-json
'01',
'01aGk=',

// correct version + empty JSON:
'01' + stringify64({}),
'01' + stringify64(''),

// correct version + non-base64 string
'01~',
].forEach($skiptoken => {
it(`should throw error for malformed $skiptoken '${$skiptoken}'`, () =>
fieldsFor(testData.forms.withrepeat)
.then((fields) => {
const submission = mockSubmission('two', testData.instances.withrepeat.two);
const query = { $skiptoken };
const originaUrl = "/withrepeat.svc/Submissions('two')/children/child"; // doesn't have to include query string
return singleRowToOData(fields, submission, 'http://localhost:8989', originaUrl, query);
})
.should.be.rejectedWith(Problem, { problemCode: 400.35, message: 'Invalid $skiptoken' }));
});

[
{
$top: 0,
Expand Down