Skip to content

Commit 242ef7c

Browse files
committed
fix(agiloft): make the redaction pipeline impossible to compose wrongly
Review finding. The create path ran describeAgiloftError on the raw body and redacted the result. That helper strips tags and collapses whitespace, so a credential containing bracket or repeated-space characters was reshaped before redaction looked for it, and a fragment could still reach the caller. That is the fifth time these three steps have been ordered wrongly in this branch, in both directions, each time in a different file. The steps are now one function: callers hand over the raw body and get back a string that is safe to relay, and there is no correct way to compose them by hand. Tests cover both reshaping transforms - a password carrying angle brackets and one carrying a double space - and assert the helper still reduces a body to its typed exception message.
1 parent a4ea758 commit 242ef7c

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

apps/sim/app/api/tools/agiloft/create_record/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage, toError } from '@sim/utils/errors'
3-
import { truncate } from '@sim/utils/string'
43
import { type NextRequest, NextResponse } from 'next/server'
54
import { agiloftCreateRecordContract } from '@/lib/api/contracts/tools/agiloft'
65
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
@@ -12,7 +11,7 @@ import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
1211
import {
1312
buildCreateRecordBody,
1413
buildCreateRecordUrl,
15-
describeAgiloftError,
14+
describeAgiloftFailure,
1615
redactAgiloftSecrets,
1716
} from '@/tools/agiloft/utils'
1817
import { executeEwRequest, resolveAgiloftInstance } from '@/tools/agiloft/utils.server'
@@ -143,7 +142,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
143142
* and the credentials are in the submitted form body, so an error page
144143
* that echoes request parameters would carry them back.
145144
*/
146-
const described = truncate(redactAgiloftSecrets(describeAgiloftError(text), params), 300)
145+
const described = describeAgiloftFailure(text, params)
147146

148147
/**
149148
* Every failure below is one Agiloft already decided on, so each is

apps/sim/tools/agiloft/utils.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
buildSelectRecordsUrl,
1919
buildUpsertRecordBody,
2020
describeAgiloftError,
21+
describeAgiloftFailure,
2122
ewCredentialBody,
2223
parseFieldList,
2324
redactAgiloftSecrets,
@@ -420,3 +421,37 @@ describe('credential redaction', () => {
420421
)
421422
})
422423
})
424+
425+
describe('describeAgiloftFailure', () => {
426+
/**
427+
* describeAgiloftError strips tags and collapses whitespace, so redacting
428+
* after it runs can miss a credential that those transforms reshaped. The
429+
* pipeline exists so the order cannot be composed wrongly at a call site.
430+
*/
431+
it('redacts a credential the HTML strip would otherwise reshape', () => {
432+
const creds = { login: 'svc.user', password: 'a<b>c' }
433+
const body = `<html><body>EWWrongDataException has occurred: submitted $password=${creds.password}</body></html>`
434+
435+
const described = describeAgiloftFailure(body, creds)
436+
437+
expect(described).not.toContain(creds.password)
438+
expect(described).toContain('[redacted]')
439+
})
440+
441+
it('redacts a credential whitespace collapsing would otherwise reshape', () => {
442+
const creds = { login: 'svc.user', password: 'two spaces' }
443+
const described = describeAgiloftFailure(`<html>echo ${creds.password}</html>`, creds)
444+
445+
expect(described).not.toContain(creds.password)
446+
expect(described).toContain('[redacted]')
447+
})
448+
449+
it('still reduces the body to its typed exception message', () => {
450+
const described = describeAgiloftFailure(
451+
'<html><body>EWWrongDataException has occurred: [task-1] Wrong format pointed to start_date</body></html>',
452+
{ login: 'svc.user', password: 'not-a-real-password' }
453+
)
454+
455+
expect(described).toBe('EWWrongDataException: Wrong format pointed to start_date')
456+
})
457+
})

apps/sim/tools/agiloft/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { truncate } from '@sim/utils/string'
12
import type {
23
AgiloftAsyncStatusParams,
34
AgiloftAttachmentInfoParams,
@@ -85,6 +86,26 @@ export function redactAgiloftSecrets(
8586
return safe
8687
}
8788

89+
/**
90+
* Turns a raw Agiloft response body into the one-line detail relayed to a
91+
* caller or written to a log.
92+
*
93+
* The ordering is the entire point of this function existing. Redaction has to
94+
* run while the text is still exactly what the server sent: `describeAgiloftError`
95+
* strips tags and collapses whitespace, and `truncate` clips, and either can
96+
* reshape a credential so it no longer matches what is being replaced - leaving
97+
* a fragment of it in the output. Composing the three by hand has gone wrong
98+
* repeatedly, in both directions, so callers hand over the raw body and get back
99+
* a string that is safe to relay.
100+
*/
101+
export function describeAgiloftFailure(
102+
rawBody: string,
103+
credentials: { login: string; password: string },
104+
maxLength = 300
105+
): string {
106+
return truncate(describeAgiloftError(redactAgiloftSecrets(rawBody, credentials)), maxLength)
107+
}
108+
88109
/** Language sent on every Agiloft call; EWLogin rejects the request without it. */
89110
export const AGILOFT_LANG = 'en'
90111

0 commit comments

Comments
 (0)