From e9ab78079a1cd7018cdca7b06e150a51281aaae3 Mon Sep 17 00:00:00 2001 From: chenzeyan54-commits Date: Sun, 27 Sep 2026 02:30:51 +0800 Subject: [PATCH] fix(#735): SOAP: send WS-Security UsernameToken headers The `WS_SECURITY` auth type exists in the schema and in the UI, but the SOAP engine never uses it: `buildEnvelope()` in `packages/backend/src/connecto... Signed-off-by: chenzeyan54-commits --- .../src/connectors/engines/soap.engine.ts | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/connectors/engines/soap.engine.ts b/packages/backend/src/connectors/engines/soap.engine.ts index 3aa6ee77..216fb5f4 100644 --- a/packages/backend/src/connectors/engines/soap.engine.ts +++ b/packages/backend/src/connectors/engines/soap.engine.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import axios from 'axios'; import * as soap from 'soap'; +import * as crypto from 'crypto'; import { XMLParser } from 'fast-xml-parser'; import { assertSafeOutboundUrl } from '../../common/ssrf.util'; @@ -81,6 +82,8 @@ export class SoapEngine { targetNamespace, soapParams, paramOrder, + config.authType, + config.authConfig, ); // Build headers @@ -161,6 +164,8 @@ export class SoapEngine { targetNamespace: string, params: Record, paramOrder: string[] = [], + authType?: string, + authConfig?: Record, ): string { const ns = targetNamespace || 'http://tempuri.org/'; @@ -174,9 +179,11 @@ export class SoapEngine { .map((key) => ` ${this.escapeXml(String(params[key]))}`) .join('\n'); + const headerXml = this.buildHeaderXml(authType, authConfig); + return ` - + ${headerXml} ${paramXml} @@ -185,6 +192,48 @@ ${paramXml} `; } + private buildHeaderXml( + authType?: string, + authConfig?: Record, + ): string { + if (authType === 'WS_SECURITY' && authConfig) { + const username = this.escapeXml(String(authConfig.username ?? '')); + const password = String(authConfig.password ?? ''); + const passwordType = authConfig.passwordType || 'PasswordText'; + + let passwordElement = ''; + let nonceElement = ''; + let createdElement = ''; + + if (passwordType === 'PasswordDigest') { + const created = new Date().toISOString(); + const nonceBytes = crypto.randomBytes(16); + const nonceBase64 = nonceBytes.toString('base64'); + const hash = crypto + .createHash('sha1') + .update(Buffer.concat([nonceBytes, Buffer.from(created, 'utf8'), Buffer.from(password, 'utf8')])) + .digest('base64'); + + passwordElement = ` ${hash}`; + nonceElement = `\n ${nonceBase64}`; + createdElement = `\n ${created}`; + } else { + passwordElement = ` ${this.escapeXml(password)}`; + } + + return ` + + + ${username} +${passwordElement}${nonceElement}${createdElement} + + + `; + } + + return ''; + } + private escapeXml(str: string): string { return str .replace(/&/g, '&') @@ -366,4 +415,4 @@ ${paramXml} } return result; } -} +} \ No newline at end of file