Skip to content

Commit aa367a4

Browse files
authored
fix(sap_concur): HMAC the token cache key and wire the sendback comment (#6794)
* fix(sap_concur): key the token cache with an HMAC and document rate casing The cache key hashed a user-chosen password with a bare SHA-256. The key never leaves the process, but a password is low-entropy enough to brute-force out of a plain digest if one ever reached a heap dump or a debug log, which is what CodeQL flags. Keying the digest with a server-side secret makes it useless without that secret. A password-hashing KDF would be the wrong tool here: this runs on every token fetch, and the goal is collision-free partitioning rather than verification of a stored credential. The body wand prompt also claimed every payload family is camelCase. Exchange rate uploads are the exception — they take a snake_case currency_sets array of from_crn_code, to_crn_code, start_date and rate — and that operation is in BODY_OPS, so the blanket claim produced bodies Concur rejects. * fix(sap_concur): wire the travel request sendback comment through the block move_travel_request accepts a documented query comment that Concur applies to the sendback action, but the params branch never passed it and the block's only comment field is gated to create_report_comment, so the value was unreachable from the UI. Uses a dedicated sendbackComment subblock rather than widening the existing comment field: that one is required for create_report_comment while this is optional, so sharing an id would both clash on required-ness and let a value bleed between the two operations. * fix(sap_concur): gate the sendback comment and merge duplicate TSDoc The sendbackComment field was conditioned only on the operation, so it rendered for submit, approve, cancel and every other workflow action even though Concur applies the comment to sendback alone. It is now gated on the action as well, and the params branch only forwards it for sendback so a value retained from an earlier sendback cannot ride along once the field is hidden. Also folds the two consecutive TSDoc blocks left above tokenCacheKey into one. Only the nearest block binds to the declaration, so the separator and collision reasoning in the earlier block was detached.
1 parent 0c34e69 commit aa367a4

2 files changed

Lines changed: 33 additions & 5 deletions

File tree

apps/sim/app/api/tools/sap_concur/shared.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { createHash } from 'node:crypto'
1+
import { createHmac } from 'node:crypto'
22
import { createLogger } from '@sim/logger'
33
import { isPrivateIpHost } from '@sim/security/ssrf'
44
import { getErrorMessage } from '@sim/utils/errors'
55
import { truncate } from '@sim/utils/string'
66
import { z } from 'zod'
77
import { coalesceLocally } from '@/lib/concurrency/singleflight'
8+
import { env } from '@/lib/core/config/env'
89
import {
910
MAX_JSON_API_RESPONSE_BYTES,
1011
secureFetchWithValidation,
@@ -226,11 +227,17 @@ function readCachedToken(key: string): SapConcurToken | undefined {
226227
*
227228
* The whole tuple is JSON-encoded before hashing rather than concatenated with a
228229
* separator, so a free-form field (clientId, companyUuid) cannot span a field boundary
229-
* and collide with a different tuple. The full sha256 digest is kept — truncating it
230-
* would lower the collision/forgery bar for no measurable gain.
230+
* and collide with a different tuple.
231+
*
232+
* Keyed with a server-side secret rather than a bare digest. The inputs include a
233+
* user-chosen password, which is low-entropy enough to brute-force from a plain SHA-256
234+
* if a key ever reached a heap dump or a debug log; an HMAC makes the key useless without
235+
* the secret. A password-hashing KDF would be the wrong tool — this runs on every token
236+
* fetch and the goal is collision-free partitioning, not verification of a stored
237+
* credential.
231238
*/
232239
function tokenCacheKey(req: SapConcurAuth): string {
233-
return createHash('sha256')
240+
return createHmac('sha256', env.INTERNAL_API_SECRET)
234241
.update(
235242
JSON.stringify([
236243
req.datacenter,

apps/sim/blocks/blocks/sap_concur.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,18 @@ Return ONLY the YYYY-MM-DD date - no explanations, no extra text.`,
10071007
condition: { field: 'operation', value: 'sap_concur_create_report_comment' },
10081008
required: { field: 'operation', value: 'sap_concur_create_report_comment' },
10091009
},
1010+
{
1011+
id: 'sendbackComment',
1012+
title: 'Sendback Comment',
1013+
type: 'long-input',
1014+
placeholder: 'Visible wherever Request comments are shown',
1015+
condition: {
1016+
field: 'operation',
1017+
value: 'sap_concur_move_travel_request',
1018+
and: { field: 'action', value: 'sendback' },
1019+
},
1020+
mode: 'advanced',
1021+
},
10101022
{
10111023
id: 'includeAllComments',
10121024
title: 'Include All Comments',
@@ -1893,7 +1905,7 @@ Return ONLY the comma-separated travel config IDs - no explanations, no extra te
18931905
enabled: true,
18941906
prompt: `Generate the JSON request body for the selected SAP Concur operation from the user's request.
18951907
1896-
Match the payload to the resource being written. Every family below is camelCase.
1908+
Match the payload to the resource being written. Every family below is camelCase EXCEPT exchange rates, which is snake_case.
18971909
18981910
Expense reports (v4): name, businessPurpose, comment, policyId, countryCode, countrySubDivisionCode, reportDate, startDate, endDate, and reportSource — reportSource is REQUIRED when updating a report and must be one of EA, MOB, OTHER, SE, TR, UI.
18991911
@@ -1905,6 +1917,8 @@ SCIM users (Identity v4.1): create and update payloads use schemas, userName, na
19051917
19061918
List items: listId, level, value, shortCode. Cash advances: amountRequested as { currency, amount }, name and userId (all required), plus optional accountCode, comment and purpose.
19071919
1920+
Exchange rates are the one snake_case family: currency_sets as an array of up to 100 entries, each { from_crn_code, to_crn_code, start_date as YYYY-MM-DD, rate }.
1921+
19081922
Omit fields the user did not describe rather than inventing identifiers.
19091923
19101924
Return ONLY the JSON object - no explanations, no extra text.`,
@@ -2262,6 +2276,8 @@ Return ONLY the JSON object - no explanations, no extra text.`,
22622276
body: params.body || undefined,
22632277
userId: params.travelRequestUserId || undefined,
22642278
companyID: params.companyID || undefined,
2279+
comment:
2280+
params.action === 'sendback' ? params.sendbackComment || undefined : undefined,
22652281
}
22662282
case 'sap_concur_list_travel_request_comments':
22672283
return { ...auth, requestUuid: params.requestUuid }
@@ -2532,6 +2548,11 @@ Return ONLY the JSON object - no explanations, no extra text.`,
25322548
description:
25332549
'Optional company identifier for a travel request workflow action (documented as companyID, distinct from companyUuid)',
25342550
},
2551+
sendbackComment: {
2552+
type: 'string',
2553+
description:
2554+
'Optional comment on a travel request workflow action — Concur applies it only to the sendback action, and it is visible wherever Request comments are shown',
2555+
},
25352556
travelRequestApprovedBefore: { type: 'string', description: 'Travel requests approved before' },
25362557
travelRequestApprovedAfter: { type: 'string', description: 'Travel requests approved after' },
25372558
travelRequestModifiedBefore: { type: 'string', description: 'Travel requests modified before' },

0 commit comments

Comments
 (0)