Skip to content
Closed
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
31 changes: 3 additions & 28 deletions routes/b2bOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,13 @@
* SPDX-License-Identifier: MIT
*/

import vm from 'node:vm'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Obsolete interpreter dependency remains

notevil has no remaining code imports but stays in the dependency manifest and lock file. Remove it with the retired evaluation path.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately left in place: if maintainers decide to keep these as intentional challenges, notevil is needed again, and dropping it now would also churn package-lock.json in a PR whose direction is still open. Happy to remove the dependency in the same follow-up that retires the challenge metadata.

import { type Request, type Response, type NextFunction } from 'express'
// @ts-expect-error FIXME due to non-existing type definitions for notevil
import { eval as safeEval } from 'notevil'
import { type Request, type Response } from 'express'

import * as challengeUtils from '../lib/challengeUtils'
import { challenges } from '../data/datacache'
import * as security from '../lib/insecurity'
import * as utils from '../lib/utils'

export function b2bOrder () {
return ({ body }: Request, res: Response, next: NextFunction) => {
if (utils.isChallengeEnabled(challenges.rceChallenge) || utils.isChallengeEnabled(challenges.rceOccupyChallenge)) {
const orderLinesData = body.orderLinesData || ''
try {
const sandbox = { safeEval, orderLinesData }
vm.createContext(sandbox)
vm.runInContext('safeEval(orderLinesData)', sandbox, { timeout: 2000 })
res.json({ cid: body.cid, orderNo: uniqueOrderNumber(), paymentDue: dateTwoWeeksFromNow() })
} catch (err) {
if (utils.getErrorMessage(err).match(/Script execution timed out.*/) != null) {
challengeUtils.solveIf(challenges.rceOccupyChallenge, () => { return true })
res.status(503)
next(new Error('Sorry, we are temporarily not available! Please try again later.'))
} else {
challengeUtils.solveIf(challenges.rceChallenge, () => { return utils.getErrorMessage(err) === 'Infinite loop detected - reached max iterations' })
next(err)
}
}
} else {
res.json({ cid: body.cid, orderNo: uniqueOrderNumber(), paymentDue: dateTwoWeeksFromNow() })
}
return ({ body }: Request, res: Response) => {
res.json({ cid: body.cid, orderNo: uniqueOrderNumber(), paymentDue: dateTwoWeeksFromNow() })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Enabled RCE challenges become unsolvable

When either RCE challenge is enabled, b2bOrder accepts every payload without invoking its solver. The challenge definitions remain active, so users can never complete them.

Learn more

These challenges are loaded into the score board unless their environment restrictions or safety mode disable them. Their only completion path was removed from b2bOrder, and the codebase contains no other solver calls for either challenge. The endpoint therefore advertises challenges that no runtime action can solve.

Example: On a normal local installation, a player submits the documented infinite-loop payload for “Blocked RCE DoS.” The endpoint returns 200, but the challenge remains unsolved forever.

Recommended fix: Retire both challenges completely from metadata, configuration, anti-cheat mappings, tests, and related model keys, or redesign safe completion conditions. If they remain enabled, add corresponding E2E coverage as required for modified challenges.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that rceChallenge / rceOccupyChallenge become unsolvable — that is the unavoidable consequence of removing server-side evaluation, and it is flagged as a maintainer decision in the PR description rather than something I retired unilaterally.

Full retirement would mean editing data/static/challenges.yml, config.schema.yml, config/fbctf.yml, lib/antiCheat.ts, models/challenge.ts keys, test/server/b2bOrderSpec.ts, and the public documentation/solutions — i.e. removing training content, not fixing a bug. The two options are: keep the intentional vulnerability (close this PR), or approve the full retirement/redesign, which I can do in a follow-up on request.

}

function uniqueOrderNumber () {
Expand Down
54 changes: 20 additions & 34 deletions test/api/b2b-order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { describe, it, before } from 'node:test'
import assert from 'node:assert/strict'
import request from 'supertest'
import type { Express } from 'express'
import { challenges } from '../../data/datacache'
import * as utils from '../../lib/utils'
import * as security from '../../lib/insecurity'
import { createTestApp } from './helpers/setup'

Expand All @@ -21,41 +19,29 @@ before(async () => {
}, { timeout: 60000 })

void describe('/b2b/v2/orders', () => {
if (utils.isChallengeEnabled(challenges.rceChallenge) || utils.isChallengeEnabled(challenges.rceOccupyChallenge)) {
void it('POST endless loop exploit in "orderLinesData" will raise explicit error', async () => {
const res = await request(app)
.post('/b2b/v2/orders')
.set(authHeader)
.send({
orderLinesData: '(function dos() { while(true); })()'
})

assert.equal(res.status, 500)
assert.ok(res.text.includes('Infinite loop detected - reached max iterations'))
})

void it('POST busy spinning regex attack does not raise an error', async () => {
const res = await request(app)
.post('/b2b/v2/orders')
.set(authHeader)
.send({
orderLinesData: '/((a+)+)b/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa")'
})
void it('POST endless loop payload in "orderLinesData" is not evaluated', async () => {
const res = await request(app)
.post('/b2b/v2/orders')
.set(authHeader)
.send({
orderLinesData: '(function dos() { while(true); })()'
})

assert.equal(res.status, 503)
})
assert.equal(res.status, 200)
assert.equal(typeof res.body.orderNo, 'string')
})

void it('POST sandbox breakout attack in "orderLinesData" will raise error', async () => {
const res = await request(app)
.post('/b2b/v2/orders')
.set(authHeader)
.send({
orderLinesData: 'this.constructor.constructor("return process")().exit()'
})
void it('POST sandbox breakout payload in "orderLinesData" is not evaluated', async () => {
const res = await request(app)
.post('/b2b/v2/orders')
.set(authHeader)
.send({
orderLinesData: 'this.constructor.constructor("return process")().exit()'
})

assert.equal(res.status, 500)
})
}
assert.equal(res.status, 200)
assert.equal(typeof res.body.orderNo, 'string')
})

void it('POST new B2B order is forbidden without authorization token', async () => {
const res = await request(app)
Expand Down
63 changes: 0 additions & 63 deletions test/cypress/e2e/b2bOrder.spec.ts

This file was deleted.

Loading