samlesa 2.16.0 → 2.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of samlesa might be problematic. Click here for more details.

@@ -70,12 +70,19 @@ const libSaml = () => {
70
70
  context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
71
71
  };
72
72
  /**
73
- * @desc Default logout request template
73
+ * @desc Default art request template
74
74
  * @type {LogoutRequestTemplate}
75
75
  */
76
76
  const defaultLogoutRequestTemplate = {
77
77
  context: '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID></samlp:LogoutRequest>',
78
78
  };
79
+ /**
80
+ * @desc Default logout request template
81
+ * @type {LogoutRequestTemplate}
82
+ */
83
+ const defaultArtifactResolveTemplate = {
84
+ context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><saml2p:ArtifactResolve xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml2:Issuer>{Issuer}</saml2:Issuer><saml2p:Artifact>{Art}</saml2p:Artifact></saml2p:ArtifactResolve></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
85
+ };
79
86
  /**
80
87
  * @desc Default AttributeStatement template
81
88
  * @type {AttributeStatementTemplate}
@@ -152,6 +159,12 @@ const libSaml = () => {
152
159
  }
153
160
  return nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA256];
154
161
  }
162
+ /**
163
+ * @private
164
+ * @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
165
+ * @param {string} sigAlg signature algorithm
166
+ * @return {string/null} signing algorithm short-hand for the module node-rsa
167
+ */
155
168
  /**
156
169
  * @private
157
170
  * @desc Get the digest algorithms by signature algorithms
@@ -196,6 +209,7 @@ const libSaml = () => {
196
209
  createXPath,
197
210
  getQueryParamByType,
198
211
  defaultLoginRequestTemplate,
212
+ defaultArtifactResolveTemplate,
199
213
  defaultLoginResponseTemplate,
200
214
  defaultAttributeStatementTemplate,
201
215
  defaultAttributeTemplate,
@@ -474,6 +488,170 @@ const libSaml = () => {
474
488
 
475
489
  return [verified, assertionNode];*/
476
490
  },
491
+ verifySignatureSoap(xml, opts) {
492
+ const { dom } = getContext();
493
+ const doc = dom.parseFromString(xml);
494
+ const docParser = new DOMParser();
495
+ let selection = [];
496
+ if (opts.isAssertion) {
497
+ // 断言模式下的专用逻辑
498
+ const assertionSignatureXpath = "./*[local-name()='Signature']";
499
+ const signatureNode = select(assertionSignatureXpath, doc.documentElement);
500
+ if (signatureNode.length === 0) {
501
+ throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
502
+ }
503
+ selection = selection.concat(signatureNode);
504
+ }
505
+ else {
506
+ // 原始的SOAP响应验证逻辑
507
+ const messageSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
508
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Signature'] | " +
509
+ "/*[local-name()='Envelope']/*[local-name()='Body']" +
510
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']/*[local-name()='Signature']";
511
+ const assertionSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
512
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
513
+ "/*[local-name()='Assertion']/*[local-name()='Signature'] | " +
514
+ "/*[local-name()='Envelope']/*[local-name()='Body']" +
515
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
516
+ "/*[local-name()='EncryptedAssertion']";
517
+ const wrappingElementsXPath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
518
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
519
+ "/*[local-name()='Assertion']/*[local-name()='Subject']" +
520
+ "/*[local-name()='SubjectConfirmation']" +
521
+ "/*[local-name()='SubjectConfirmationData']" +
522
+ "//*[local-name()='Assertion' or local-name()='Signature']";
523
+ const messageSignatureNode = select(messageSignatureXpath, doc);
524
+ const assertionSignatureNode = select(assertionSignatureXpath, doc);
525
+ const wrappingElementNode = select(wrappingElementsXPath, doc);
526
+ // 检测包装攻击
527
+ if (wrappingElementNode.length !== 0) {
528
+ throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
529
+ }
530
+ // 保证响应中至少有一个签名
531
+ if (messageSignatureNode.length === 0 && assertionSignatureNode.length === 0) {
532
+ throw new Error('ERR_ZERO_SIGNATURE');
533
+ }
534
+ selection = selection.concat(messageSignatureNode, assertionSignatureNode);
535
+ }
536
+ for (const signatureNode of selection) {
537
+ const sig = new SignedXml();
538
+ let verified = false;
539
+ sig.signatureAlgorithm = opts.signatureAlgorithm;
540
+ if (!opts.keyFile && !opts.metadata) {
541
+ throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
542
+ }
543
+ if (opts.keyFile) {
544
+ sig.publicCert = fs.readFileSync(opts.keyFile, 'utf-8');
545
+ }
546
+ if (opts.metadata) {
547
+ const certificateNodes = select(".//*[local-name(.)='X509Certificate']", signatureNode);
548
+ // 获取元数据中的证书
549
+ let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
550
+ // 规范化元数据证书
551
+ if (Array.isArray(metadataCert)) {
552
+ metadataCert = flattenDeep(metadataCert);
553
+ }
554
+ else if (typeof metadataCert === 'string') {
555
+ metadataCert = [metadataCert];
556
+ }
557
+ metadataCert = metadataCert.map(utility.normalizeCerString);
558
+ // 检查证书可用性
559
+ if (certificateNodes.length === 0 && metadataCert.length === 0) {
560
+ throw new Error('NO_SELECTED_CERTIFICATE');
561
+ }
562
+ // 响应中有证书节点
563
+ if (certificateNodes.length !== 0) {
564
+ // 安全获取证书数据
565
+ let x509CertificateData = '';
566
+ if (certificateNodes[0].firstChild) {
567
+ x509CertificateData = certificateNodes[0].firstChild.data;
568
+ }
569
+ else if (certificateNodes[0].textContent) {
570
+ x509CertificateData = certificateNodes[0].textContent;
571
+ }
572
+ const x509Certificate = utility.normalizeCerString(x509CertificateData);
573
+ // 验证证书匹配
574
+ if (metadataCert.length >= 1 &&
575
+ !metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
576
+ throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
577
+ }
578
+ sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
579
+ }
580
+ else {
581
+ // 使用元数据中的第一个证书
582
+ sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
583
+ }
584
+ }
585
+ // 加载签名
586
+ sig.loadSignature(signatureNode);
587
+ // 使用原始 XML 进行验证
588
+ verified = sig.checkSignature(xml);
589
+ console.log("签名验证结果:", verified);
590
+ if (!verified) {
591
+ console.error("签名验证失败");
592
+ throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
593
+ }
594
+ // 检查签名引用
595
+ if (!(sig.getSignedReferences().length >= 1)) {
596
+ throw new Error('NO_SIGNATURE_REFERENCES');
597
+ }
598
+ const signedVerifiedXML = sig.getSignedReferences()[0];
599
+ const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'text/xml');
600
+ const rootNode = verifiedDoc.documentElement;
601
+ console.log("签名引用根节点:", rootNode.localName);
602
+ // 断言模式专用返回逻辑
603
+ if (opts.isAssertion) {
604
+ if (rootNode.localName === 'Assertion') {
605
+ return [true, rootNode.toString(), false];
606
+ }
607
+ else {
608
+ throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
609
+ }
610
+ }
611
+ // 处理已验证的签名
612
+ if (rootNode.localName === 'ArtifactResponse') {
613
+ // 在 ArtifactResponse 中查找 Response
614
+ const responseNodes = select("./*[local-name()='Response']", rootNode);
615
+ if (responseNodes.length === 0) {
616
+ console.warn("ArtifactResponse 中没有找到 Response 元素");
617
+ continue;
618
+ }
619
+ const responseNode = responseNodes[0];
620
+ // 在 Response 中查找断言
621
+ const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", responseNode);
622
+ const assertions = select("./*[local-name()='Assertion']", responseNode);
623
+ if (encryptedAssertions.length === 1) {
624
+ return [true, encryptedAssertions[0].toString(), true];
625
+ }
626
+ if (assertions.length === 1) {
627
+ return [true, assertions[0].toString(), false];
628
+ }
629
+ }
630
+ // 直接处理 Response
631
+ else if (rootNode.localName === 'Response') {
632
+ const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", rootNode);
633
+ const assertions = select("./*[local-name()='Assertion']", rootNode);
634
+ if (encryptedAssertions.length === 1) {
635
+ return [true, encryptedAssertions[0].toString(), true];
636
+ }
637
+ if (assertions.length === 1) {
638
+ return [true, assertions[0].toString(), false];
639
+ }
640
+ }
641
+ // 直接处理 Assertion
642
+ else if (rootNode.localName === 'Assertion') {
643
+ return [true, rootNode.toString(), false];
644
+ }
645
+ // 直接处理 EncryptedAssertion
646
+ else if (rootNode.localName === 'EncryptedAssertion') {
647
+ return [true, rootNode.toString(), true];
648
+ }
649
+ else {
650
+ console.warn("未知的根节点类型:", rootNode.localName);
651
+ }
652
+ }
653
+ throw new Error('ERR_ZERO_SIGNATURE');
654
+ },
477
655
  /**
478
656
  * @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
479
657
  * @param {string} use type of certificate (e.g. signing, encrypt)
@@ -538,6 +716,26 @@ const libSaml = () => {
538
716
  throw new Error(`SAML 签名失败: ${error.message}`);
539
717
  }
540
718
  },
719
+ /* constructMessageSignature(
720
+ octetString: string,
721
+ key: string,
722
+ passphrase?: string,
723
+ isBase64?: boolean,
724
+ signingAlgorithm?: string
725
+ ) {
726
+ // Default returning base64 encoded signature
727
+ // Embed with node-rsa module
728
+ const decryptedKey = new nrsa(
729
+ utility.readPrivateKey(key, passphrase),
730
+ undefined,
731
+ {
732
+ signingScheme: getSigningScheme(signingAlgorithm),
733
+ }
734
+ );
735
+ const signature = decryptedKey.sign(octetString);
736
+ // Use private key to sign data
737
+ return isBase64 !== false ? signature.toString('base64') : signature;
738
+ },*/
541
739
  verifyMessageSignature(metadata, octetString, signature, verifyAlgorithm) {
542
740
  const signCert = metadata.getX509Certificate(certUse.signing);
543
741
  const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
@@ -660,6 +858,61 @@ const libSaml = () => {
660
858
  });
661
859
  });
662
860
  },
861
+ /**
862
+ * 解密 SOAP 响应中的加密断言
863
+ * @param self 当前实体(SP 或 IdP)
864
+ * @param entireXML 完整的 SOAP XML 响应
865
+ * @returns [解密后的完整 SOAP XML, 解密后的断言 XML]
866
+ */
867
+ async decryptAssertionSoap(self, entireXML) {
868
+ const { dom } = getContext();
869
+ try {
870
+ // 1. 解析 XML
871
+ const doc = dom.parseFromString(entireXML);
872
+ // 2. 定位加密断言
873
+ const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
874
+ "/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
875
+ "/*[local-name()='EncryptedAssertion']", doc);
876
+ if (!encryptedAssertions || encryptedAssertions.length === 0) {
877
+ throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
878
+ }
879
+ if (encryptedAssertions.length > 1) {
880
+ console.warn('发现多个加密断言,仅处理第一个');
881
+ }
882
+ const encAssertionNode = encryptedAssertions[0];
883
+ // 3. 准备解密密钥
884
+ const privateKey = utility.readPrivateKey(self.entitySetting.encPrivateKey, self.entitySetting.encPrivateKeyPass);
885
+ // 4. 解密断言
886
+ const decryptedAssertion = await new Promise((resolve, reject) => {
887
+ xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
888
+ if (err) {
889
+ console.error('解密错误:', err);
890
+ return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
891
+ }
892
+ if (!result) {
893
+ return reject(new Error('ERR_EMPTY_DECRYPTED_ASSERTION'));
894
+ }
895
+ resolve(result);
896
+ });
897
+ });
898
+ // 5. 创建解密断言的 DOM
899
+ const decryptedDoc = dom.parseFromString(decryptedAssertion);
900
+ const decryptedAssertionNode = decryptedDoc.documentElement;
901
+ // 6. 替换加密断言为解密后的断言
902
+ const parentNode = encAssertionNode.parentNode;
903
+ if (!parentNode) {
904
+ throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
905
+ }
906
+ parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
907
+ // 7. 序列化更新后的文档
908
+ const updatedSoapXml = doc.toString();
909
+ return [updatedSoapXml, decryptedAssertion];
910
+ }
911
+ catch (error) {
912
+ console.error('SOAP断言解密失败:', error);
913
+ throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
914
+ }
915
+ },
663
916
  /**
664
917
  * @desc Check if the xml string is valid and bounded
665
918
  */
@@ -102,6 +102,13 @@ export class IdpMetadata extends Metadata {
102
102
  attributePath: [],
103
103
  attributes: ['Location']
104
104
  },
105
+ {
106
+ key: 'artifactResolutionService',
107
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ArtifactResolutionService'],
108
+ index: ['Binding'],
109
+ attributePath: [],
110
+ attributes: ['Location']
111
+ },
105
112
  ]);
106
113
  }
107
114
  /**
@@ -130,4 +137,19 @@ export class IdpMetadata extends Metadata {
130
137
  }
131
138
  return this.meta.singleSignOnService;
132
139
  }
140
+ /**
141
+ * @desc Get the entity endpoint for single ArtifactResolutionService
142
+ * @param {string} binding protocol binding (e.g. redirect, post)
143
+ * @return {string/object} location
144
+ */
145
+ getArtifactResolutionService(binding) {
146
+ if (isString(binding)) {
147
+ const bindName = namespace.binding[binding];
148
+ const service = this.meta.artifactResolutionService[bindName];
149
+ if (service) {
150
+ return service;
151
+ }
152
+ }
153
+ return this.meta.artifactResolutionService;
154
+ }
133
155
  }
@@ -70,6 +70,18 @@ export class SpMetadata extends Metadata {
70
70
  descriptors.SingleLogoutService.push([{ _attr: attr }]);
71
71
  });
72
72
  }
73
+ if (isNonEmptyArray(artifactResolutionService)) {
74
+ artifactResolutionService.forEach(a => {
75
+ const attr = {
76
+ Binding: a.Binding,
77
+ Location: a.Location,
78
+ };
79
+ if (a.isDefault) {
80
+ attr.isDefault = true;
81
+ }
82
+ descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
83
+ });
84
+ }
73
85
  if (isNonEmptyArray(assertionConsumerService)) {
74
86
  let indexCount = 0;
75
87
  assertionConsumerService.forEach(a => {
@@ -150,21 +162,6 @@ export class SpMetadata extends Metadata {
150
162
  descriptors.AttributeConsumingService.push(attrConsumingService);
151
163
  });
152
164
  }
153
- if (isNonEmptyArray(artifactResolutionService)) {
154
- artifactResolutionService.forEach(a => {
155
- const attr = {
156
- Binding: a.Binding,
157
- Location: a.Location,
158
- };
159
- if (a.isDefault) {
160
- attr.isDefault = true;
161
- }
162
- descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
163
- });
164
- }
165
- else {
166
- console.warn('Construct identity provider - missing endpoint of ArtifactResolutionService');
167
- }
168
165
  // handle element order
169
166
  const existedElements = elementsOrder.filter(name => isNonEmptyArray(descriptors[name]));
170
167
  existedElements.forEach(name => {
@@ -193,6 +190,11 @@ export class SpMetadata extends Metadata {
193
190
  key: 'assertionConsumerService',
194
191
  localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
195
192
  attributes: ['Binding', 'Location', 'isDefault', 'index'],
193
+ },
194
+ {
195
+ key: 'artifactResolutionService',
196
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
197
+ attributes: ['Binding', 'Location', 'isDefault', 'index'],
196
198
  }
197
199
  ]);
198
200
  }
@@ -1,17 +1,17 @@
1
1
  /**
2
- * @file metadata.ts
3
- * @author tngan
4
- * @desc An abstraction for metadata of identity provider and service provider
5
- */
2
+ * @file metadata.ts
3
+ * @author tngan
4
+ * @desc An abstraction for metadata of identity provider and service provider
5
+ */
6
6
  import * as fs from 'fs';
7
7
  import { namespace } from './urn.js';
8
8
  import { extract } from './extractor.js';
9
9
  import { isString } from './utility.js';
10
10
  export default class Metadata {
11
11
  /**
12
- * @param {string | Buffer} xml
13
- * @param {object} extraParse for custom metadata extractor
14
- */
12
+ * @param {string | Buffer} xml
13
+ * @param {object} extraParse for custom metadata extractor
14
+ */
15
15
  constructor(xml, extraParse = []) {
16
16
  this.xmlString = xml.toString();
17
17
  this.meta = extract(this.xmlString, extraParse.concat([
@@ -66,46 +66,46 @@ export default class Metadata {
66
66
  }
67
67
  }
68
68
  /**
69
- * @desc Get the metadata in xml format
70
- * @return {string} metadata in xml format
71
- */
69
+ * @desc Get the metadata in xml format
70
+ * @return {string} metadata in xml format
71
+ */
72
72
  getMetadata() {
73
73
  return this.xmlString;
74
74
  }
75
75
  /**
76
- * @desc Export the metadata to specific file
77
- * @param {string} exportFile is the output file path
78
- */
76
+ * @desc Export the metadata to specific file
77
+ * @param {string} exportFile is the output file path
78
+ */
79
79
  exportMetadata(exportFile) {
80
80
  fs.writeFileSync(exportFile, this.xmlString);
81
81
  }
82
82
  /**
83
- * @desc Get the entityID in metadata
84
- * @return {string} entityID
85
- */
83
+ * @desc Get the entityID in metadata
84
+ * @return {string} entityID
85
+ */
86
86
  getEntityID() {
87
87
  return this.meta.entityID;
88
88
  }
89
89
  /**
90
- * @desc Get the x509 certificate declared in entity metadata
91
- * @param {string} use declares the type of certificate
92
- * @return {string} certificate in string format
93
- */
90
+ * @desc Get the x509 certificate declared in entity metadata
91
+ * @param {string} use declares the type of certificate
92
+ * @return {string} certificate in string format
93
+ */
94
94
  getX509Certificate(use) {
95
95
  return this.meta.certificate[use] || null;
96
96
  }
97
97
  /**
98
- * @desc Get the support NameID format declared in entity metadata
99
- * @return {array} support NameID format
100
- */
98
+ * @desc Get the support NameID format declared in entity metadata
99
+ * @return {array} support NameID format
100
+ */
101
101
  getNameIDFormat() {
102
102
  return this.meta.nameIDFormat;
103
103
  }
104
104
  /**
105
- * @desc Get the entity endpoint for single logout service
106
- * @param {string} binding e.g. redirect, post
107
- * @return {string/object} location
108
- */
105
+ * @desc Get the entity endpoint for single logout service
106
+ * @param {string} binding e.g. redirect, post
107
+ * @return {string/object} location
108
+ */
109
109
  getSingleLogoutService(binding) {
110
110
  if (binding && isString(binding)) {
111
111
  const bindType = namespace.binding[binding];
@@ -121,10 +121,31 @@ export default class Metadata {
121
121
  return this.meta.singleLogoutService;
122
122
  }
123
123
  /**
124
- * @desc Get the support bindings
125
- * @param {[string]} services
126
- * @return {[string]} support bindings
127
- */
124
+ * @desc Get the entity endpoint for single logout service
125
+ * @param {string} binding e.g. redirect, post
126
+ * @return {string/object} location
127
+ */
128
+ getArtifactResolutionService(binding) {
129
+ if (binding && isString(binding)) {
130
+ const bindType = namespace.binding[binding];
131
+ console.log(this.meta);
132
+ console.log("看一下---------------------");
133
+ let artifactResolutionService = this.meta.artifactResolutionService;
134
+ if (!(artifactResolutionService instanceof Array)) {
135
+ artifactResolutionService = [artifactResolutionService];
136
+ }
137
+ const service = artifactResolutionService.find(obj => obj.binding === bindType);
138
+ if (service) {
139
+ return service.location;
140
+ }
141
+ }
142
+ return this.meta.artifactResolutionService;
143
+ }
144
+ /**
145
+ * @desc Get the support bindings
146
+ * @param {[string]} services
147
+ * @return {[string]} support bindings
148
+ */
128
149
  getSupportBindings(services) {
129
150
  let supportBindings = [];
130
151
  if (services) {
@@ -0,0 +1,100 @@
1
+
2
+ <!-- Schema for the SOAP/1.1 envelope
3
+
4
+ Portions © 2001 DevelopMentor.
5
+ © 2001 W3C (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved.
6
+
7
+ This document is governed by the W3C Software License [1] as described in the FAQ [2].
8
+ [1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
9
+ [2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
10
+ By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
11
+
12
+ Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
13
+
14
+ 1. The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
15
+
16
+ 2. Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, a short notice of the following form (hypertext is preferred, text is permitted) should be used within the body of any redistributed or derivative code: "Copyright © 2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/"
17
+
18
+ 3. Notice of any changes or modifications to the W3C files, including the date changes were made. (We recommend you provide URIs to the location from which the code is derived.)
19
+
20
+ Original W3C files; http://www.w3.org/2001/06/soap-envelope
21
+ Changes made:
22
+ - reverted namespace to http://schemas.xmlsoap.org/soap/envelope/
23
+ - reverted mustUnderstand to only allow 0 and 1 as lexical values
24
+ - made encodingStyle a global attribute 20020825
25
+ - removed default value from mustUnderstand attribute declaration
26
+
27
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
28
+
29
+ COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
30
+
31
+ The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the software without specific, written prior permission. Title to copyright in this software and any associated documentation will at all times remain with copyright holders.
32
+
33
+ -->
34
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/" targetNamespace="http://schemas.xmlsoap.org/soap/envelope/">
35
+ <!-- Envelope, header and body -->
36
+ <xs:element name="Envelope" type="tns:Envelope"/>
37
+ <xs:complexType name="Envelope">
38
+ <xs:sequence>
39
+ <xs:element ref="tns:Header" minOccurs="0"/>
40
+ <xs:element ref="tns:Body" minOccurs="1"/>
41
+ <xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
42
+ </xs:sequence>
43
+ <xs:anyAttribute namespace="##other" processContents="lax"/>
44
+ </xs:complexType>
45
+ <xs:element name="Header" type="tns:Header"/>
46
+ <xs:complexType name="Header">
47
+ <xs:sequence>
48
+ <xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
49
+ </xs:sequence>
50
+ <xs:anyAttribute namespace="##other" processContents="lax"/>
51
+ </xs:complexType>
52
+ <xs:element name="Body" type="tns:Body"/>
53
+ <xs:complexType name="Body">
54
+ <xs:sequence>
55
+ <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
56
+ </xs:sequence>
57
+ <xs:anyAttribute namespace="##any" processContents="lax">
58
+ <xs:annotation>
59
+ <xs:documentation> Prose in the spec does not specify that attributes are allowed on the Body element </xs:documentation>
60
+ </xs:annotation>
61
+ </xs:anyAttribute>
62
+ </xs:complexType>
63
+ <!-- Global Attributes. The following attributes are intended to be usable via qualified attribute names on any complex type referencing them. -->
64
+ <xs:attribute name="mustUnderstand">
65
+ <xs:simpleType>
66
+ <xs:restriction base="xs:boolean">
67
+ <xs:pattern value="0|1"/>
68
+ </xs:restriction>
69
+ </xs:simpleType>
70
+ </xs:attribute>
71
+ <xs:attribute name="actor" type="xs:anyURI"/>
72
+ <xs:simpleType name="encodingStyle">
73
+ <xs:annotation>
74
+ <xs:documentation> 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification </xs:documentation>
75
+ </xs:annotation>
76
+ <xs:list itemType="xs:anyURI"/>
77
+ </xs:simpleType>
78
+ <xs:attribute name="encodingStyle" type="tns:encodingStyle"/>
79
+ <xs:attributeGroup name="encodingStyle">
80
+ <xs:attribute ref="tns:encodingStyle"/>
81
+ </xs:attributeGroup>
82
+ <xs:element name="Fault" type="tns:Fault"/>
83
+ <xs:complexType name="Fault" final="extension">
84
+ <xs:annotation>
85
+ <xs:documentation> Fault reporting structure </xs:documentation>
86
+ </xs:annotation>
87
+ <xs:sequence>
88
+ <xs:element name="faultcode" type="xs:QName"/>
89
+ <xs:element name="faultstring" type="xs:string"/>
90
+ <xs:element name="faultactor" type="xs:anyURI" minOccurs="0"/>
91
+ <xs:element name="detail" type="tns:detail" minOccurs="0"/>
92
+ </xs:sequence>
93
+ </xs:complexType>
94
+ <xs:complexType name="detail">
95
+ <xs:sequence>
96
+ <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
97
+ </xs:sequence>
98
+ <xs:anyAttribute namespace="##any" processContents="lax"/>
99
+ </xs:complexType>
100
+ </xs:schema>
@@ -13,9 +13,37 @@ const schemas = [
13
13
  'xenc-schema.xsd',
14
14
  'saml-schema-metadata-2.0.xsd',
15
15
  'saml-schema-ecp-2.0.xsd',
16
- 'saml-schema-dce-2.0.xsd'
16
+ 'saml-schema-dce-2.0.xsd',
17
+ 'env.xsd'
17
18
  ];
19
+ function detectXXEIndicators(samlString) {
20
+ const xxePatterns = [
21
+ /<!DOCTYPE\s[^>]*>/i,
22
+ /<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i,
23
+ /&[a-zA-Z0-9._-]+;/g,
24
+ /SYSTEM\s*=/i,
25
+ /PUBLIC\s*=/i,
26
+ /file:\/\//,
27
+ /\.dtd['"]?/
28
+ ];
29
+ const matches = {};
30
+ xxePatterns.forEach((pattern, index) => {
31
+ const found = samlString.match(pattern);
32
+ if (found) {
33
+ matches[`pattern_${index}`] = {
34
+ pattern: pattern.toString(),
35
+ matches: found
36
+ };
37
+ }
38
+ });
39
+ return Object.keys(matches).length > 0 ? matches : null;
40
+ }
18
41
  export const validate = async (xml) => {
42
+ const indicators = detectXXEIndicators(xml);
43
+ if (indicators) {
44
+ console.error('XXE风险特征:', indicators);
45
+ throw new Error('ERR_EXCEPTION_VALIDATE_XML');
46
+ }
19
47
  const schemaPath = path.resolve(__dirname, 'schema');
20
48
  const [schema, ...preload] = await Promise.all(schemas.map(async (file) => ({
21
49
  fileName: file,