Skip to content

Commit d434909

Browse files
committed
Track expected preceding interaction with URLs
(before a challenge is solved, and print percentage in cheat logs. Just informative for testing, no impact on score yet!)
1 parent 10ab7d7 commit d434909

File tree

2 files changed

+33
-1
lines changed

2 files changed

+33
-1
lines changed

lib/antiCheat.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { readFixes } from '../routes/vulnCodeFixes'
1010
import { type Challenge } from '../data/types'
1111
import { getCodeChallenges } from './codingChallenges'
1212
import logger from './logger'
13+
import { type NextFunction, type Request, type Response } from 'express'
14+
import * as utils from './utils'
1315
const median = require('median')
1416

1517
const coupledChallenges = { // TODO prevent also near-identical challenges (e.g. all null byte file access or dom xss + bonus payload etc.) from counting as cheating
@@ -23,6 +25,25 @@ const trivialChallenges = ['errorHandlingChallenge', 'privacyPolicyChallenge', '
2325

2426
const solves: Array<{ challenge: any, phase: string, timestamp: Date, cheatScore: number }> = [{ challenge: {}, phase: 'server start', timestamp: new Date(), cheatScore: 0 }] // seed with server start timestamp
2527

28+
const preSolveInteractions: Array<{ challengeKey: any, urlFragments: string[], interactions: number }> = [
29+
{
30+
challengeKey: 'missingEncodingChallenge',
31+
urlFragments: ['/assets/public/images/uploads/%F0%9F%98%BC-'],
32+
interactions: 0
33+
}
34+
]
35+
36+
exports.checkForPreSolveInteractions = () => ({ url }: Request, res: Response, next: NextFunction) => {
37+
preSolveInteractions.forEach((preSolveInteraction) => {
38+
preSolveInteraction.urlFragments.forEach((urlFragment) => {
39+
if (utils.endsWith(url, urlFragment)) {
40+
preSolveInteraction.interactions++
41+
}
42+
})
43+
})
44+
next()
45+
}
46+
2647
export const calculateCheatScore = (challenge: Challenge) => {
2748
const timestamp = new Date()
2849
let cheatScore = 0
@@ -37,7 +58,14 @@ export const calculateCheatScore = (challenge: Challenge) => {
3758
const minutesSincePreviousSolve = (timestamp.getTime() - previous().timestamp.getTime()) / 60000
3859
cheatScore += Math.max(0, 1 - (minutesSincePreviousSolve / minutesExpectedToSolve))
3960

40-
logger.info(`Cheat score for ${areCoupled(challenge, previous().challenge) ? 'coupled ' : (isTrivial(challenge) ? 'trivial ' : '')}${challenge.tutorialOrder ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)}min (expected ~${minutesExpectedToSolve}min) with${config.get('challenges.showHints') ? '' : 'out'} hints allowed: ${cheatScore < 0.33 ? colors.green(cheatScore.toString()) : (cheatScore < 0.66 ? colors.yellow(cheatScore.toString()) : colors.red(cheatScore.toString()))}`)
61+
const preSolveInteraction = preSolveInteractions.find((preSolveInteraction) => preSolveInteraction.challengeKey === challenge.key)
62+
let percentPrecedingInteraction = -1
63+
if (preSolveInteraction) {
64+
percentPrecedingInteraction = preSolveInteraction.interactions / (preSolveInteraction.urlFragments.length * challenge.difficulty)
65+
// TODO Add impact on actual cheat score
66+
}
67+
68+
logger.info(`Cheat score for ${areCoupled(challenge, previous().challenge) ? 'coupled ' : (isTrivial(challenge) ? 'trivial ' : '')}${challenge.tutorialOrder ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)}min (expected ~${minutesExpectedToSolve}min) with${config.get('challenges.showHints') ? '' : 'out'} hints allowed${ percentPrecedingInteraction > -1 ? (' and ' + percentPrecedingInteraction * 100 + '% expected preceding URL interaction') : ''}: ${cheatScore < 0.33 ? colors.green(cheatScore.toString()) : (cheatScore < 0.66 ? colors.yellow(cheatScore.toString()) : colors.red(cheatScore.toString()))}`)
4169
solves.push({ challenge, phase: 'hack it', timestamp, cheatScore })
4270
return cheatScore
4371
}

server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const memory = require('./routes/memory')
118118
const chatbot = require('./routes/chatbot')
119119
const locales = require('./data/static/locales.json')
120120
const i18n = require('i18n')
121+
const antiCheat = require('./lib/antiCheat')
121122

122123
const appName = config.get('application.customMetricsPrefix')
123124
const startupGauge = new client.Gauge({
@@ -205,6 +206,9 @@ restoreOverwrittenFilesWithOriginals().then(() => {
205206
/* robots.txt */
206207
app.use(robots({ UserAgent: '*', Disallow: '/ftp' }))
207208

209+
/* Check for any URLs having been called that would be expected for challenge solving without cheating */
210+
app.use(antiCheat.checkForPreSolveInteractions())
211+
208212
/* Checks for challenges solved by retrieving a file implicitly or explicitly */
209213
app.use('/assets/public/images/padding', verify.accessControlChallenges())
210214
app.use('/assets/public/images/products', verify.accessControlChallenges())

0 commit comments

Comments
 (0)