blun-king-cli 9.0.1 → 9.1.0
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.
- package/LIESMICH.txt +31 -6
- package/README.md +35 -4
- package/bin/launcher-mode.js +13 -2
- package/bin/launcher-runtime.js +103 -31
- package/bin/plugin-bootstrap.js +11 -4
- package/bin/private-paths.js +1577 -10
- package/bin/update-notice.js +197 -46
- package/blun.mjs +7422 -3448
- package/dist-web/assets/{CodeBlockNode-Bsvp6ZEy.js → CodeBlockNode-CXnTLMNr.js} +1 -1
- package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-ta7hLjia.js → CodeBlockShell.vue_vue_type_style_index_0_lang-CCF8Ntnq.js} +1 -1
- package/dist-web/assets/{DesignSystemView-BmDDe28A.js → DesignSystemView-BbMOu-XO.js} +1 -1
- package/dist-web/assets/{Tooltip-nEEfaYVY.js → Tooltip-CFQEzqC-.js} +1 -1
- package/dist-web/assets/abnfDiagram-N423BO3Z-CsByy-GW.js +1 -0
- package/dist-web/assets/abnfDiagram-N423BO3Z-Z1APZG5m.js +1 -0
- package/dist-web/assets/arc-C-LXCaja.js +1 -0
- package/dist-web/assets/arc-C4VyWloT.js +1 -0
- package/dist-web/assets/architectureDiagram-T3A2C74G-DZq4L18e.js +36 -0
- package/dist-web/assets/architectureDiagram-T3A2C74G-DpddbUcM.js +36 -0
- package/dist-web/assets/blockDiagram-VBNYF7ZC-BV4sIf98.js +132 -0
- package/dist-web/assets/blockDiagram-VBNYF7ZC-BqccOSMr.js +132 -0
- package/dist-web/assets/{c4Diagram-AAUBKEIU-DtzacMnD.js → c4Diagram-5PPSVZJV-0MRkZlgP.js} +1 -1
- package/dist-web/assets/{c4Diagram-AAUBKEIU-D1VdMx8d.js → c4Diagram-5PPSVZJV-OHAcJrTy.js} +1 -1
- package/dist-web/assets/channel-B2VOQOI-.js +1 -0
- package/dist-web/assets/channel-DKjwM05G.js +1 -0
- package/dist-web/assets/{chunk-ND2GUHAM-DJoY9wNJ.js → chunk-2GRJ4B5K-DqJHs8Fw.js} +1 -1
- package/dist-web/assets/{chunk-ND2GUHAM-C4-rwdcv.js → chunk-2GRJ4B5K-oO6nqeLA.js} +1 -1
- package/dist-web/assets/{chunk-QZHKN3VN-C7YGaShq.js → chunk-2Q5K7J3B-B1nCd91o.js} +1 -1
- package/dist-web/assets/{chunk-QZHKN3VN-_Rz9_NuS.js → chunk-2Q5K7J3B-BC0Hsfqy.js} +1 -1
- package/dist-web/assets/chunk-5RXB4S5H-BfZa4yDH.js +231 -0
- package/dist-web/assets/chunk-5RXB4S5H-neG70kZx.js +231 -0
- package/dist-web/assets/{chunk-FMBD7UC4-DlLNDUx6.js → chunk-5VM5RSS4-l7biGqeH.js} +1 -1
- package/dist-web/assets/{chunk-FMBD7UC4-B2zs_Y-d.js → chunk-5VM5RSS4-u00AcpiJ.js} +1 -1
- package/dist-web/assets/chunk-6Q2QTUOP-BO-F11Gs.js +88 -0
- package/dist-web/assets/chunk-6Q2QTUOP-RUVRPBzG.js +88 -0
- package/dist-web/assets/{chunk-727SXJPM-Dn5wkDO_.js → chunk-GF5L2VYU-Ba-86KTd.js} +5 -5
- package/dist-web/assets/{chunk-727SXJPM-Dk2Xa3bA.js → chunk-GF5L2VYU-BtC69fw0.js} +5 -5
- package/dist-web/assets/{chunk-4BX2VUAB-DxZViaHS.js → chunk-JWPE2WC7-DIOubEqr.js} +1 -1
- package/dist-web/assets/{chunk-4BX2VUAB-WqrE2gaw.js → chunk-JWPE2WC7-u8Pcfhwh.js} +1 -1
- package/dist-web/assets/{chunk-2J33WTMH-Dm_s0_4f.js → chunk-KBJHAD2P-DDUeH_Ba.js} +1 -1
- package/dist-web/assets/{chunk-2J33WTMH-w4sdiKFO.js → chunk-KBJHAD2P-DS3BL64y.js} +1 -1
- package/dist-web/assets/chunk-RYQCIY6F-B9gFMVbS.js +1 -0
- package/dist-web/assets/chunk-RYQCIY6F-xg3EO0le.js +1 -0
- package/dist-web/assets/{chunk-55IACEB6-B5dE1-Um.js → chunk-XXDRQBXY-B60H0rRC.js} +1 -1
- package/dist-web/assets/{chunk-55IACEB6-B12zGIFz.js → chunk-XXDRQBXY-Bn1Xz6TQ.js} +1 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +1 -0
- package/dist-web/assets/classDiagram-JCYQIIEL-BNU7Qn4k.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BNU7Qn4k.js +1 -0
- package/dist-web/assets/{cose-bilkent-S5V4N54A-Bi19n8m-.js → cose-bilkent-JH36ORCC-4Ybddg08.js} +1 -1
- package/dist-web/assets/{cose-bilkent-S5V4N54A-c8o8NFp5.js → cose-bilkent-JH36ORCC-DqigO3T4.js} +1 -1
- package/dist-web/assets/cynefin-VYW2F7L2-C-EfYT07.js +178 -0
- package/dist-web/assets/cynefin-VYW2F7L2-rpD93tQ-.js +178 -0
- package/dist-web/assets/cynefinDiagram-MW4NZA55-DWZPLJcD.js +62 -0
- package/dist-web/assets/cynefinDiagram-MW4NZA55-DbTqnkVJ.js +62 -0
- package/dist-web/assets/dagre-VZM6K2ZE-DcM8aEVY.js +4 -0
- package/dist-web/assets/dagre-VZM6K2ZE-M_HVNtqP.js +4 -0
- package/dist-web/assets/diagram-7IWD3JNH-DoNximNF.js +30 -0
- package/dist-web/assets/diagram-7IWD3JNH-UymgsBNa.js +30 -0
- package/dist-web/assets/{diagram-KO2AKTUF-CFQs13i5.js → diagram-B4RE2ZJO-BVA2EhnC.js} +3 -3
- package/dist-web/assets/{diagram-KO2AKTUF-agFIuPJW.js → diagram-B4RE2ZJO-BiYu5WPF.js} +3 -3
- package/dist-web/assets/{diagram-LMA3HP47-TMIXizxR.js → diagram-LBJQPF4R-BD3kkgOO.js} +2 -2
- package/dist-web/assets/{diagram-LMA3HP47-He5zlyMt.js → diagram-LBJQPF4R-m3a1Euj3.js} +2 -2
- package/dist-web/assets/diagram-Q27KOJAE-D1mYQ_cI.js +24 -0
- package/dist-web/assets/diagram-Q27KOJAE-hy_T73PK.js +24 -0
- package/dist-web/assets/diagram-UB23O5K3-BWkoQZ4A.js +41 -0
- package/dist-web/assets/diagram-UB23O5K3-BtX8u6HG.js +41 -0
- package/dist-web/assets/ebnfDiagram-BXEA7PRR-BKs9JUHW.js +1 -0
- package/dist-web/assets/ebnfDiagram-BXEA7PRR-CBAnFkRF.js +1 -0
- package/dist-web/assets/erDiagram-JOGREHBK-3vKcWsxm.js +85 -0
- package/dist-web/assets/erDiagram-JOGREHBK-BIpd6dTf.js +85 -0
- package/dist-web/assets/flowDiagram-UKHOOZJN-D9wBbHm2.js +156 -0
- package/dist-web/assets/flowDiagram-UKHOOZJN-Z83i2UWI.js +156 -0
- package/dist-web/assets/ganttDiagram-PKOTCBZU-Cd_dViF-.js +292 -0
- package/dist-web/assets/ganttDiagram-PKOTCBZU-DFrb_S1d.js +292 -0
- package/dist-web/assets/{gitGraphDiagram-PVQCEYII-BSDFfwdY.js → gitGraphDiagram-DS77QQ5N-BiEEtTo7.js} +3 -3
- package/dist-web/assets/{gitGraphDiagram-PVQCEYII-DMbTATyw.js → gitGraphDiagram-DS77QQ5N-Dp3Ar-hi.js} +3 -3
- package/dist-web/assets/{graph-BwjfAU3j.js → graph-CAUpWmoK.js} +1 -1
- package/dist-web/assets/{graph--OzhPTMs.js → graph-DOmOIIwC.js} +1 -1
- package/dist-web/assets/{index-W6VnSrkB.js → index-Ba1OvLjf.js} +2 -2
- package/dist-web/assets/{index-CkVgGDpx.js → index-CiPA8Kex.js} +4 -4
- package/dist-web/assets/{index-BzK4vJBu.js → index-DdEakFJM.js} +1 -1
- package/dist-web/assets/{index10-Czg_oF-W.js → index10-CNr4U-Nw.js} +1 -1
- package/dist-web/assets/{index11-BjzCZuCr.js → index11-Dto_yZNz.js} +1 -1
- package/dist-web/assets/{index5-DlVoNkCD.js → index5-B2PldaO2.js} +1 -1
- package/dist-web/assets/{index6-BxiXA2yo.js → index6-CBdZmmyx.js} +1 -1
- package/dist-web/assets/{index7-BPdza6d1.js → index7-Dz9oCsUF.js} +1 -1
- package/dist-web/assets/{index8-Q_XzGC1Z.js → index8-B6k25jtU.js} +2 -2
- package/dist-web/assets/{infoDiagram-5YYISTIA-2JF3XEdD.js → infoDiagram-6WML65LV-BSa2g04G.js} +1 -1
- package/dist-web/assets/{infoDiagram-5YYISTIA-UrQusrJq.js → infoDiagram-6WML65LV-D4ph2sIH.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-YF4QCWOH-7RVUL-vB.js → ishikawaDiagram-WSZJBQD7-DjKjbMm9.js} +5 -5
- package/dist-web/assets/{ishikawaDiagram-YF4QCWOH-C5OstRVV.js → ishikawaDiagram-WSZJBQD7-Hm36UOQe.js} +5 -5
- package/dist-web/assets/{journeyDiagram-JHISSGLW-B34fCMu9.js → journeyDiagram-NVQOT4AX-DttHDfPQ.js} +1 -1
- package/dist-web/assets/{journeyDiagram-JHISSGLW-yctdo4bX.js → journeyDiagram-NVQOT4AX-MvWWPNDV.js} +1 -1
- package/dist-web/assets/{kanban-definition-UN3LZRKU-BS4FBZsQ.js → kanban-definition-27J2QSJJ-C5gB-W0d.js} +7 -7
- package/dist-web/assets/{kanban-definition-UN3LZRKU-CxBCv27E.js → kanban-definition-27J2QSJJ-Cu-E_cXA.js} +5 -5
- package/dist-web/assets/layout-CvP6o9AK.js +1 -0
- package/dist-web/assets/layout-D-LzfAck.js +1 -0
- package/dist-web/assets/{linear-3mB6q2-g.js → linear-B3HWgQI2.js} +1 -1
- package/dist-web/assets/{linear-BKCnUo31.js → linear-BRGe4-d6.js} +1 -1
- package/dist-web/assets/map-CWKZqZjr.js +1 -0
- package/dist-web/assets/map-DxJ2ADlA.js +1 -0
- package/dist-web/assets/mermaid.core-D-9Sx1No.js +314 -0
- package/dist-web/assets/mermaidParser.worker-PTFfzpoB.js +318 -0
- package/dist-web/assets/{mindmap-definition-RKZ34NQL-BmJZpjh6.js → mindmap-definition-FAOFIHXS-C4vnms3i.js} +2 -2
- package/dist-web/assets/{mindmap-definition-RKZ34NQL-1mjWUcf6.js → mindmap-definition-FAOFIHXS-Cg_hoVIq.js} +1 -1
- package/dist-web/assets/pegDiagram-VL7TDLO6-B2fFEFUH.js +1 -0
- package/dist-web/assets/pegDiagram-VL7TDLO6-CSX9hitD.js +1 -0
- package/dist-web/assets/pieDiagram-7S7Q4E2Y-0L0mxahZ.js +39 -0
- package/dist-web/assets/pieDiagram-7S7Q4E2Y-C7T0Cmw1.js +39 -0
- package/dist-web/assets/{quadrantDiagram-W4KKPZXB-BmT_qM09.js → quadrantDiagram-CIZ2JOQS-BBy7w4kD.js} +3 -3
- package/dist-web/assets/{quadrantDiagram-W4KKPZXB-C0kPuyNP.js → quadrantDiagram-CIZ2JOQS-U483z0er.js} +3 -3
- package/dist-web/assets/railroadDiagram-AXF67PYL-D80a2_DJ.js +1 -0
- package/dist-web/assets/railroadDiagram-AXF67PYL-DX8y0MzW.js +1 -0
- package/dist-web/assets/{requirementDiagram-4Y6WPE33-C0YP_Amt.js → requirementDiagram-LRYGKXZP-CeyHfq8t.js} +3 -3
- package/dist-web/assets/{requirementDiagram-4Y6WPE33-CZrxH1Y2.js → requirementDiagram-LRYGKXZP-DGD5J_4k.js} +3 -3
- package/dist-web/assets/sankeyDiagram-W5VNT64P-CCISkkrR.js +40 -0
- package/dist-web/assets/sankeyDiagram-W5VNT64P-CT5PdZz2.js +40 -0
- package/dist-web/assets/sequenceDiagram-SI44F4Z6-BeMzHWXI.js +162 -0
- package/dist-web/assets/sequenceDiagram-SI44F4Z6-jQfm2XS7.js +162 -0
- package/dist-web/assets/sizeCapture-X5ZJPWSS-BQsTdtrv.js +1 -0
- package/dist-web/assets/sizeCapture-X5ZJPWSS-lRZsmCoS.js +1 -0
- package/dist-web/assets/stateDiagram-OKZ733FA-ClCgTdUm.js +1 -0
- package/dist-web/assets/stateDiagram-OKZ733FA-Cv59z5YT.js +1 -0
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-R8jTc--F.js +1 -0
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +1 -0
- package/dist-web/assets/swimlanes-SLNWSIFB-BCdOUN05.js +2 -0
- package/dist-web/assets/swimlanes-SLNWSIFB-Cu5l7SuR.js +1 -0
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +8 -0
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-CalrsgPK.js +8 -0
- package/dist-web/assets/{timeline-definition-PNZ67QCA-DsVZdrZa.js → timeline-definition-Z64GVDOM-CMjnh4bu.js} +4 -4
- package/dist-web/assets/{timeline-definition-PNZ67QCA-DnB6Usl0.js → timeline-definition-Z64GVDOM-DpWutSKa.js} +4 -4
- package/dist-web/assets/vennDiagram-T6HMQDX7-DKJW_8oU.js +34 -0
- package/dist-web/assets/vennDiagram-T6HMQDX7-DVC5JVsc.js +34 -0
- package/dist-web/assets/{vue.runtime.esm-bundler-pgHGheCW.js → vue.runtime.esm-bundler-BwwRtQwQ.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-YWT4CUSO-oBauTCEw.js → wardleyDiagram-T6FBY63Y-BK2mrq7L.js} +3 -3
- package/dist-web/assets/{wardleyDiagram-YWT4CUSO-DCiyDQNB.js → wardleyDiagram-T6FBY63Y-oUvgepFn.js} +3 -3
- package/dist-web/assets/xychartDiagram-ELKLHX3M-DI6n79q0.js +7 -0
- package/dist-web/assets/xychartDiagram-ELKLHX3M-DUJu9F7-.js +7 -0
- package/dist-web/index.html +1 -1
- package/dist-web/vis/index.html.gz +0 -0
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/package.json +7 -2
- package/telegram-plugin/dist/bridge.mjs +6 -4
- package/telegram-plugin/dist/mcp-server.mjs +236 -71
- package/dist-web/assets/arc-4xPfymbf.js +0 -1
- package/dist-web/assets/arc-Doj0wRZ0.js +0 -1
- package/dist-web/assets/architectureDiagram-3BPJPVTR-BGih283a.js +0 -36
- package/dist-web/assets/architectureDiagram-3BPJPVTR-CQ09RrbH.js +0 -36
- package/dist-web/assets/blockDiagram-GPEHLZMM-3ShMf1gu.js +0 -132
- package/dist-web/assets/blockDiagram-GPEHLZMM-CAtDjkJt.js +0 -132
- package/dist-web/assets/channel-DNkUo9e6.js +0 -1
- package/dist-web/assets/channel-DcnUTIGB.js +0 -1
- package/dist-web/assets/chunk-AQP2D5EJ-FS8-f8lF.js +0 -231
- package/dist-web/assets/chunk-AQP2D5EJ-UKhb4V4r.js +0 -231
- package/dist-web/assets/classDiagram-4FO5ZUOK-B1ZO8EbE.js +0 -1
- package/dist-web/assets/classDiagram-4FO5ZUOK-xk6cM9Kq.js +0 -1
- package/dist-web/assets/classDiagram-v2-Q7XG4LA2-B1ZO8EbE.js +0 -1
- package/dist-web/assets/classDiagram-v2-Q7XG4LA2-xk6cM9Kq.js +0 -1
- package/dist-web/assets/dagre-BM42HDAG-CmUO9qKN.js +0 -4
- package/dist-web/assets/dagre-BM42HDAG-DLMfLCoV.js +0 -4
- package/dist-web/assets/diagram-2AECGRRQ-DQTst0OH.js +0 -43
- package/dist-web/assets/diagram-2AECGRRQ-L74iLMqd.js +0 -43
- package/dist-web/assets/diagram-5GNKFQAL-BiBm7CJR.js +0 -10
- package/dist-web/assets/diagram-5GNKFQAL-DoE6q7H1.js +0 -10
- package/dist-web/assets/diagram-OG6HWLK6-BHCt1E0n.js +0 -24
- package/dist-web/assets/diagram-OG6HWLK6-DRyxjFMa.js +0 -24
- package/dist-web/assets/erDiagram-TEJ5UH35-BoDczjGG.js +0 -85
- package/dist-web/assets/erDiagram-TEJ5UH35-yd6SOv_7.js +0 -85
- package/dist-web/assets/flowDiagram-I6XJVG4X-DHoceI7r.js +0 -162
- package/dist-web/assets/flowDiagram-I6XJVG4X-DkYNQv0R.js +0 -162
- package/dist-web/assets/ganttDiagram-6RSMTGT7-Cg1TQBc0.js +0 -292
- package/dist-web/assets/ganttDiagram-6RSMTGT7-Z6klo7KK.js +0 -292
- package/dist-web/assets/layout-C1ojF0zw.js +0 -1
- package/dist-web/assets/layout-SsrduOYp.js +0 -1
- package/dist-web/assets/mermaid.core-D_lKT0dR.js +0 -309
- package/dist-web/assets/mermaidParser.worker-Dx4jPi9z.js +0 -313
- package/dist-web/assets/pieDiagram-4H26LBE5-BU41t2A_.js +0 -30
- package/dist-web/assets/pieDiagram-4H26LBE5-BkwDAvbt.js +0 -30
- package/dist-web/assets/sankeyDiagram-5OEKKPKP-De_o7hDr.js +0 -40
- package/dist-web/assets/sankeyDiagram-5OEKKPKP-Dvuj6bMA.js +0 -40
- package/dist-web/assets/sequenceDiagram-3UESZ5HK-DuCrgPyA.js +0 -162
- package/dist-web/assets/sequenceDiagram-3UESZ5HK-VAxffBe7.js +0 -162
- package/dist-web/assets/stateDiagram-AJRCARHV-CYLupxM7.js +0 -1
- package/dist-web/assets/stateDiagram-AJRCARHV-nDExQydZ.js +0 -1
- package/dist-web/assets/stateDiagram-v2-BHNVJYJU-BhEc7Thx.js +0 -1
- package/dist-web/assets/stateDiagram-v2-BHNVJYJU-BzwD_BIl.js +0 -1
- package/dist-web/assets/vennDiagram-CIIHVFJN-BBHQmCFf.js +0 -34
- package/dist-web/assets/vennDiagram-CIIHVFJN-Cfj-K9NW.js +0 -34
- package/dist-web/assets/wardley-L42UT6IY-BJFn8eDD.js +0 -173
- package/dist-web/assets/wardley-L42UT6IY-BSu8-4hg.js +0 -173
- package/dist-web/assets/xychartDiagram-2RQKCTM6-C2iZpl9I.js +0 -7
- package/dist-web/assets/xychartDiagram-2RQKCTM6-Db_VoDBV.js +0 -7
package/bin/private-paths.js
CHANGED
|
@@ -1,33 +1,1599 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
3
4
|
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { gzipSync } = require('node:zlib');
|
|
4
7
|
|
|
5
8
|
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
6
9
|
const PRIVATE_FILE_MODE = 0o600;
|
|
10
|
+
const PRIVATE_TREE_MAX_DEPTH = 64;
|
|
11
|
+
const PRIVATE_TREE_MAX_ENTRIES = 4096;
|
|
12
|
+
const PRIVATE_TREE_MAX_PATH_LENGTH = 4096;
|
|
13
|
+
const WINDOWS_ACL_OK_SENTINEL = 'BLUN_PRIVATE_ACL_OK_V1';
|
|
14
|
+
const WINDOWS_SID_PATTERN = /S-\d-\d+(?:-\d+)+/gi;
|
|
15
|
+
const WINDOWS_LOCAL_DOS_PATH_PATTERN = /^[A-Za-z]:\\/u;
|
|
16
|
+
const WINDOWS_RESERVED_PATH_SEGMENT = /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9]|LPT[1-9])(?:\..*)?$/iu;
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
const WINDOWS_ACL_CSHARP = String.raw`using Microsoft.Win32.SafeHandles;
|
|
19
|
+
using System;
|
|
20
|
+
using System.Collections.Generic;
|
|
21
|
+
using System.ComponentModel;
|
|
22
|
+
using System.IO;
|
|
23
|
+
using System.Runtime.InteropServices;
|
|
24
|
+
using System.Security.AccessControl;
|
|
25
|
+
using System.Security.Principal;
|
|
26
|
+
using System.Text;
|
|
27
|
+
|
|
28
|
+
public static class BlunPrivateAclNative
|
|
29
|
+
{
|
|
30
|
+
private const uint GENERIC_READ = 0x80000000;
|
|
31
|
+
private const uint GENERIC_WRITE = 0x40000000;
|
|
32
|
+
private const uint DELETE = 0x00010000;
|
|
33
|
+
private const uint READ_CONTROL = 0x00020000;
|
|
34
|
+
private const uint WRITE_DAC = 0x00040000;
|
|
35
|
+
private const uint SYNCHRONIZE = 0x00100000;
|
|
36
|
+
private const uint CREATE_NEW = 1;
|
|
37
|
+
private const uint OPEN_EXISTING = 3;
|
|
38
|
+
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
|
|
39
|
+
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
|
|
40
|
+
private const uint FILE_ATTRIBUTE_DEVICE = 0x00000040;
|
|
41
|
+
private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
|
|
42
|
+
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
|
43
|
+
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
|
44
|
+
private const uint FILE_DIRECTORY_FILE = 0x00000001;
|
|
45
|
+
private const uint FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020;
|
|
46
|
+
private const uint FILE_OPEN_REPARSE_POINT = 0x00200000;
|
|
47
|
+
private const uint FILE_OPEN_IF = 3;
|
|
48
|
+
private const uint OBJ_CASE_INSENSITIVE = 0x00000040;
|
|
49
|
+
private const uint SE_FILE_OBJECT = 1;
|
|
50
|
+
private const uint OWNER_SECURITY_INFORMATION = 0x00000001;
|
|
51
|
+
private const uint DACL_SECURITY_INFORMATION = 0x00000004;
|
|
52
|
+
private const uint PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000;
|
|
53
|
+
private const int FILE_DISPOSITION_INFO_CLASS = 4;
|
|
54
|
+
private const int FILE_STANDARD_INFO_CLASS = 1;
|
|
55
|
+
private const int FILE_REMOTE_PROTOCOL_INFO_CLASS = 13;
|
|
56
|
+
private const int FILE_ID_BOTH_DIRECTORY_INFORMATION_CLASS = 37;
|
|
57
|
+
private const int FILE_STANDARD_INFO_BUFFER_SIZE = 24;
|
|
58
|
+
private const int REMOTE_PROTOCOL_INFO_BUFFER_SIZE = 256;
|
|
59
|
+
private const int DIRECTORY_INFORMATION_BUFFER_SIZE = 65536;
|
|
60
|
+
private const int DIRECTORY_INFORMATION_FILE_ID_OFFSET = 96;
|
|
61
|
+
private const int DIRECTORY_INFORMATION_NAME_OFFSET = 104;
|
|
62
|
+
private const int MAX_TREE_DEPTH = 64;
|
|
63
|
+
private const int MAX_TREE_ENTRIES = 4096;
|
|
64
|
+
private const int MAX_TREE_PATH_LENGTH = 4096;
|
|
65
|
+
private const int ERROR_INVALID_PARAMETER = 87;
|
|
66
|
+
private const uint FILE_TYPE_DISK = 1;
|
|
67
|
+
private const int STATUS_NO_MORE_FILES = unchecked((int)0x80000006);
|
|
68
|
+
|
|
69
|
+
private sealed class TreeEntry
|
|
70
|
+
{
|
|
71
|
+
public string Name;
|
|
72
|
+
public uint Attributes;
|
|
73
|
+
public ulong FileId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private sealed class HeldTreeHandle
|
|
77
|
+
{
|
|
78
|
+
public SafeFileHandle Handle;
|
|
79
|
+
public string Path;
|
|
80
|
+
public bool Directory;
|
|
81
|
+
public string Owner;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
85
|
+
private struct SecurityAttributes
|
|
86
|
+
{
|
|
87
|
+
public int Length;
|
|
88
|
+
public IntPtr SecurityDescriptor;
|
|
89
|
+
[MarshalAs(UnmanagedType.Bool)] public bool InheritHandle;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
93
|
+
private struct FileTime
|
|
94
|
+
{
|
|
95
|
+
public uint Low;
|
|
96
|
+
public uint High;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
100
|
+
private struct ByHandleFileInformation
|
|
101
|
+
{
|
|
102
|
+
public uint FileAttributes;
|
|
103
|
+
public FileTime CreationTime;
|
|
104
|
+
public FileTime LastAccessTime;
|
|
105
|
+
public FileTime LastWriteTime;
|
|
106
|
+
public uint VolumeSerialNumber;
|
|
107
|
+
public uint FileSizeHigh;
|
|
108
|
+
public uint FileSizeLow;
|
|
109
|
+
public uint NumberOfLinks;
|
|
110
|
+
public uint FileIndexHigh;
|
|
111
|
+
public uint FileIndexLow;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
115
|
+
private struct FileDispositionInfo
|
|
116
|
+
{
|
|
117
|
+
[MarshalAs(UnmanagedType.Bool)] public bool DeleteFile;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
121
|
+
private struct UnicodeString
|
|
122
|
+
{
|
|
123
|
+
public ushort Length;
|
|
124
|
+
public ushort MaximumLength;
|
|
125
|
+
public IntPtr Buffer;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
129
|
+
private struct ObjectAttributes
|
|
130
|
+
{
|
|
131
|
+
public int Length;
|
|
132
|
+
public IntPtr RootDirectory;
|
|
133
|
+
public IntPtr ObjectName;
|
|
134
|
+
public uint Attributes;
|
|
135
|
+
public IntPtr SecurityDescriptor;
|
|
136
|
+
public IntPtr SecurityQualityOfService;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
140
|
+
private struct IoStatusBlock
|
|
141
|
+
{
|
|
142
|
+
public IntPtr Status;
|
|
143
|
+
public UIntPtr Information;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true,
|
|
147
|
+
EntryPoint = "CreateFileW")]
|
|
148
|
+
private static extern SafeFileHandle CreateFileNew(
|
|
149
|
+
string fileName,
|
|
150
|
+
uint desiredAccess,
|
|
151
|
+
uint shareMode,
|
|
152
|
+
ref SecurityAttributes securityAttributes,
|
|
153
|
+
uint creationDisposition,
|
|
154
|
+
uint flagsAndAttributes,
|
|
155
|
+
IntPtr templateFile);
|
|
156
|
+
|
|
157
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true,
|
|
158
|
+
EntryPoint = "CreateFileW")]
|
|
159
|
+
private static extern SafeFileHandle CreateFileExisting(
|
|
160
|
+
string fileName,
|
|
161
|
+
uint desiredAccess,
|
|
162
|
+
uint shareMode,
|
|
163
|
+
IntPtr securityAttributes,
|
|
164
|
+
uint creationDisposition,
|
|
165
|
+
uint flagsAndAttributes,
|
|
166
|
+
IntPtr templateFile);
|
|
167
|
+
|
|
168
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
169
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
170
|
+
private static extern bool GetFileInformationByHandle(
|
|
171
|
+
SafeFileHandle handle,
|
|
172
|
+
out ByHandleFileInformation information);
|
|
173
|
+
|
|
174
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
175
|
+
private static extern uint GetFileType(SafeFileHandle handle);
|
|
176
|
+
|
|
177
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
178
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
179
|
+
private static extern bool GetFileInformationByHandleEx(
|
|
180
|
+
SafeFileHandle handle,
|
|
181
|
+
int informationClass,
|
|
182
|
+
IntPtr information,
|
|
183
|
+
uint bufferSize);
|
|
184
|
+
|
|
185
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
186
|
+
private static extern uint GetFinalPathNameByHandleW(
|
|
187
|
+
SafeFileHandle handle,
|
|
188
|
+
StringBuilder path,
|
|
189
|
+
uint pathLength,
|
|
190
|
+
uint flags);
|
|
191
|
+
|
|
192
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
193
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
194
|
+
private static extern bool SetFileInformationByHandle(
|
|
195
|
+
SafeFileHandle handle,
|
|
196
|
+
int informationClass,
|
|
197
|
+
ref FileDispositionInfo information,
|
|
198
|
+
uint bufferSize);
|
|
199
|
+
|
|
200
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
201
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
202
|
+
private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
203
|
+
string stringSecurityDescriptor,
|
|
204
|
+
uint stringSdRevision,
|
|
205
|
+
out IntPtr securityDescriptor,
|
|
206
|
+
out uint securityDescriptorSize);
|
|
207
|
+
|
|
208
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
209
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
210
|
+
private static extern bool GetSecurityDescriptorDacl(
|
|
211
|
+
IntPtr securityDescriptor,
|
|
212
|
+
[MarshalAs(UnmanagedType.Bool)] out bool daclPresent,
|
|
213
|
+
out IntPtr dacl,
|
|
214
|
+
[MarshalAs(UnmanagedType.Bool)] out bool daclDefaulted);
|
|
215
|
+
|
|
216
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
217
|
+
private static extern uint SetSecurityInfo(
|
|
218
|
+
SafeFileHandle handle,
|
|
219
|
+
uint objectType,
|
|
220
|
+
uint securityInformation,
|
|
221
|
+
IntPtr owner,
|
|
222
|
+
IntPtr group,
|
|
223
|
+
IntPtr dacl,
|
|
224
|
+
IntPtr sacl);
|
|
225
|
+
|
|
226
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
227
|
+
private static extern uint GetSecurityInfo(
|
|
228
|
+
SafeFileHandle handle,
|
|
229
|
+
uint objectType,
|
|
230
|
+
uint securityInformation,
|
|
231
|
+
out IntPtr owner,
|
|
232
|
+
out IntPtr group,
|
|
233
|
+
out IntPtr dacl,
|
|
234
|
+
out IntPtr sacl,
|
|
235
|
+
out IntPtr securityDescriptor);
|
|
236
|
+
|
|
237
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
238
|
+
private static extern uint GetSecurityDescriptorLength(IntPtr securityDescriptor);
|
|
239
|
+
|
|
240
|
+
[DllImport("kernel32.dll")]
|
|
241
|
+
private static extern IntPtr LocalFree(IntPtr memory);
|
|
242
|
+
|
|
243
|
+
[DllImport("ntdll.dll")]
|
|
244
|
+
private static extern int NtCreateFile(
|
|
245
|
+
out IntPtr fileHandle,
|
|
246
|
+
uint desiredAccess,
|
|
247
|
+
ref ObjectAttributes objectAttributes,
|
|
248
|
+
out IoStatusBlock ioStatusBlock,
|
|
249
|
+
IntPtr allocationSize,
|
|
250
|
+
uint fileAttributes,
|
|
251
|
+
uint shareAccess,
|
|
252
|
+
uint createDisposition,
|
|
253
|
+
uint createOptions,
|
|
254
|
+
IntPtr eaBuffer,
|
|
255
|
+
uint eaLength);
|
|
256
|
+
|
|
257
|
+
[DllImport("ntdll.dll")]
|
|
258
|
+
private static extern uint RtlNtStatusToDosError(int status);
|
|
259
|
+
|
|
260
|
+
[DllImport("ntdll.dll")]
|
|
261
|
+
private static extern int NtQueryDirectoryFile(
|
|
262
|
+
SafeFileHandle fileHandle,
|
|
263
|
+
IntPtr eventHandle,
|
|
264
|
+
IntPtr apcRoutine,
|
|
265
|
+
IntPtr apcContext,
|
|
266
|
+
out IoStatusBlock ioStatusBlock,
|
|
267
|
+
IntPtr fileInformation,
|
|
268
|
+
uint length,
|
|
269
|
+
int fileInformationClass,
|
|
270
|
+
[MarshalAs(UnmanagedType.U1)] bool returnSingleEntry,
|
|
271
|
+
IntPtr fileName,
|
|
272
|
+
[MarshalAs(UnmanagedType.U1)] bool restartScan);
|
|
273
|
+
|
|
274
|
+
private static HashSet<string> ExpectedSids(string userSid)
|
|
275
|
+
{
|
|
276
|
+
SecurityIdentifier user = new SecurityIdentifier(userSid);
|
|
277
|
+
HashSet<string> expected = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
278
|
+
expected.Add(user.Value);
|
|
279
|
+
expected.Add("S-1-5-18");
|
|
280
|
+
expected.Add("S-1-5-32-544");
|
|
281
|
+
return expected;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private static string ExactDaclSddl(string userSid, bool directory)
|
|
285
|
+
{
|
|
286
|
+
string flags = directory ? "OICI" : "";
|
|
287
|
+
string[] trustees = new string[] {
|
|
288
|
+
new SecurityIdentifier(userSid).Value,
|
|
289
|
+
"S-1-5-18",
|
|
290
|
+
"S-1-5-32-544"
|
|
291
|
+
};
|
|
292
|
+
HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
293
|
+
StringBuilder builder = new StringBuilder("D:P");
|
|
294
|
+
foreach (string trustee in trustees)
|
|
295
|
+
{
|
|
296
|
+
if (!seen.Add(trustee)) continue;
|
|
297
|
+
builder.Append("(A;").Append(flags).Append(";FA;;;")
|
|
298
|
+
.Append(trustee).Append(")");
|
|
299
|
+
}
|
|
300
|
+
return builder.ToString();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private static IntPtr CreateExactDescriptor(string userSid, bool directory)
|
|
304
|
+
{
|
|
305
|
+
IntPtr descriptor;
|
|
306
|
+
uint descriptorSize;
|
|
307
|
+
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
308
|
+
ExactDaclSddl(userSid, directory), 1, out descriptor, out descriptorSize))
|
|
309
|
+
{
|
|
310
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
311
|
+
}
|
|
312
|
+
return descriptor;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private static void SetExactDacl(SafeFileHandle handle, string userSid, bool directory)
|
|
316
|
+
{
|
|
317
|
+
IntPtr descriptor = CreateExactDescriptor(userSid, directory);
|
|
318
|
+
try
|
|
319
|
+
{
|
|
320
|
+
bool present;
|
|
321
|
+
bool defaulted;
|
|
322
|
+
IntPtr dacl;
|
|
323
|
+
if (!GetSecurityDescriptorDacl(descriptor, out present, out dacl, out defaulted)
|
|
324
|
+
|| !present || dacl == IntPtr.Zero)
|
|
325
|
+
{
|
|
326
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
327
|
+
}
|
|
328
|
+
uint result = SetSecurityInfo(
|
|
329
|
+
handle,
|
|
330
|
+
SE_FILE_OBJECT,
|
|
331
|
+
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
|
|
332
|
+
IntPtr.Zero,
|
|
333
|
+
IntPtr.Zero,
|
|
334
|
+
dacl,
|
|
335
|
+
IntPtr.Zero);
|
|
336
|
+
if (result != 0) throw new Win32Exception((int)result);
|
|
337
|
+
}
|
|
338
|
+
finally
|
|
339
|
+
{
|
|
340
|
+
LocalFree(descriptor);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private static RawSecurityDescriptor ReadAccessDescriptor(SafeFileHandle handle)
|
|
345
|
+
{
|
|
346
|
+
IntPtr owner;
|
|
347
|
+
IntPtr group;
|
|
348
|
+
IntPtr dacl;
|
|
349
|
+
IntPtr sacl;
|
|
350
|
+
IntPtr descriptor;
|
|
351
|
+
uint result = GetSecurityInfo(
|
|
352
|
+
handle,
|
|
353
|
+
SE_FILE_OBJECT,
|
|
354
|
+
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
|
355
|
+
out owner,
|
|
356
|
+
out group,
|
|
357
|
+
out dacl,
|
|
358
|
+
out sacl,
|
|
359
|
+
out descriptor);
|
|
360
|
+
if (result != 0) throw new Win32Exception((int)result);
|
|
361
|
+
try
|
|
362
|
+
{
|
|
363
|
+
uint length = GetSecurityDescriptorLength(descriptor);
|
|
364
|
+
if (length == 0 || length > 65536) throw new InvalidDataException();
|
|
365
|
+
byte[] bytes = new byte[length];
|
|
366
|
+
Marshal.Copy(descriptor, bytes, 0, (int)length);
|
|
367
|
+
return new RawSecurityDescriptor(bytes, 0);
|
|
368
|
+
}
|
|
369
|
+
finally
|
|
370
|
+
{
|
|
371
|
+
LocalFree(descriptor);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private static string ValidateOwner(
|
|
376
|
+
SafeFileHandle handle,
|
|
377
|
+
HashSet<string> expectedSids)
|
|
378
|
+
{
|
|
379
|
+
RawSecurityDescriptor descriptor = ReadAccessDescriptor(handle);
|
|
380
|
+
if (descriptor.Owner == null || !expectedSids.Contains(descriptor.Owner.Value))
|
|
381
|
+
throw new UnauthorizedAccessException();
|
|
382
|
+
return descriptor.Owner.Value;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private static void VerifyExactDacl(
|
|
386
|
+
SafeFileHandle handle,
|
|
387
|
+
string userSid,
|
|
388
|
+
bool directory,
|
|
389
|
+
string expectedOwner)
|
|
390
|
+
{
|
|
391
|
+
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
392
|
+
RawSecurityDescriptor descriptor = ReadAccessDescriptor(handle);
|
|
393
|
+
if (descriptor.Owner == null
|
|
394
|
+
|| !String.Equals(descriptor.Owner.Value, expectedOwner, StringComparison.OrdinalIgnoreCase))
|
|
395
|
+
throw new UnauthorizedAccessException();
|
|
396
|
+
if ((descriptor.ControlFlags & ControlFlags.DiscretionaryAclProtected) == 0)
|
|
397
|
+
throw new UnauthorizedAccessException();
|
|
398
|
+
RawAcl acl = descriptor.DiscretionaryAcl;
|
|
399
|
+
if (acl == null || acl.Count != expectedSids.Count)
|
|
400
|
+
throw new UnauthorizedAccessException();
|
|
401
|
+
|
|
402
|
+
AceFlags expectedFlags = directory
|
|
403
|
+
? AceFlags.ObjectInherit | AceFlags.ContainerInherit
|
|
404
|
+
: AceFlags.None;
|
|
405
|
+
HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
406
|
+
foreach (GenericAce genericAce in acl)
|
|
407
|
+
{
|
|
408
|
+
CommonAce ace = genericAce as CommonAce;
|
|
409
|
+
if (ace == null
|
|
410
|
+
|| ace.AceQualifier != AceQualifier.AccessAllowed
|
|
411
|
+
|| ace.AccessMask != (int)FileSystemRights.FullControl
|
|
412
|
+
|| ace.AceFlags != expectedFlags
|
|
413
|
+
|| ace.SecurityIdentifier == null
|
|
414
|
+
|| !expectedSids.Contains(ace.SecurityIdentifier.Value)
|
|
415
|
+
|| !seen.Add(ace.SecurityIdentifier.Value))
|
|
416
|
+
{
|
|
417
|
+
throw new UnauthorizedAccessException();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!seen.SetEquals(expectedSids)) throw new UnauthorizedAccessException();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private static ByHandleFileInformation ValidateHandle(
|
|
424
|
+
SafeFileHandle handle,
|
|
425
|
+
bool directory)
|
|
426
|
+
{
|
|
427
|
+
ByHandleFileInformation information;
|
|
428
|
+
if (!GetFileInformationByHandle(handle, out information))
|
|
429
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
430
|
+
bool actualDirectory = (information.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
431
|
+
if (actualDirectory != directory
|
|
432
|
+
|| (information.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
433
|
+
{
|
|
434
|
+
throw new UnauthorizedAccessException();
|
|
435
|
+
}
|
|
436
|
+
if (GetFileType(handle) != FILE_TYPE_DISK)
|
|
437
|
+
throw new UnauthorizedAccessException();
|
|
438
|
+
if (!directory) VerifyFreshFileState(handle, false, true);
|
|
439
|
+
VerifyLocalHandle(handle);
|
|
440
|
+
return information;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private static ulong FileId(ByHandleFileInformation information)
|
|
444
|
+
{
|
|
445
|
+
return ((ulong)information.FileIndexHigh << 32) | information.FileIndexLow;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private static void VerifyTreeHandleIdentity(SafeFileHandle handle, ulong expectedFileId)
|
|
449
|
+
{
|
|
450
|
+
ByHandleFileInformation information;
|
|
451
|
+
if (!GetFileInformationByHandle(handle, out information))
|
|
452
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
453
|
+
ulong actualFileId = FileId(information);
|
|
454
|
+
if (expectedFileId == 0 || actualFileId == 0 || actualFileId != expectedFileId)
|
|
455
|
+
throw new UnauthorizedAccessException();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private static void VerifyFreshFileState(
|
|
459
|
+
SafeFileHandle handle,
|
|
460
|
+
bool expectedDeletePending,
|
|
461
|
+
bool requireSingleLink)
|
|
462
|
+
{
|
|
463
|
+
IntPtr buffer = Marshal.AllocHGlobal(FILE_STANDARD_INFO_BUFFER_SIZE);
|
|
464
|
+
try
|
|
465
|
+
{
|
|
466
|
+
for (int offset = 0; offset < FILE_STANDARD_INFO_BUFFER_SIZE; offset += 4)
|
|
467
|
+
Marshal.WriteInt32(buffer, offset, 0);
|
|
468
|
+
if (!GetFileInformationByHandleEx(
|
|
469
|
+
handle,
|
|
470
|
+
FILE_STANDARD_INFO_CLASS,
|
|
471
|
+
buffer,
|
|
472
|
+
FILE_STANDARD_INFO_BUFFER_SIZE))
|
|
473
|
+
{
|
|
474
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
475
|
+
}
|
|
476
|
+
uint numberOfLinks = unchecked((uint)Marshal.ReadInt32(buffer, 16));
|
|
477
|
+
bool deletePending = Marshal.ReadByte(buffer, 20) != 0;
|
|
478
|
+
bool directory = Marshal.ReadByte(buffer, 21) != 0;
|
|
479
|
+
if (directory
|
|
480
|
+
|| deletePending != expectedDeletePending
|
|
481
|
+
|| (requireSingleLink ? numberOfLinks != 1 : numberOfLinks > 1))
|
|
482
|
+
{
|
|
483
|
+
throw new UnauthorizedAccessException();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
finally
|
|
487
|
+
{
|
|
488
|
+
Marshal.FreeHGlobal(buffer);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private static void VerifyLocalHandle(SafeFileHandle handle)
|
|
493
|
+
{
|
|
494
|
+
IntPtr buffer = Marshal.AllocHGlobal(REMOTE_PROTOCOL_INFO_BUFFER_SIZE);
|
|
495
|
+
try
|
|
496
|
+
{
|
|
497
|
+
for (int offset = 0; offset < REMOTE_PROTOCOL_INFO_BUFFER_SIZE; offset += 4)
|
|
498
|
+
Marshal.WriteInt32(buffer, offset, 0);
|
|
499
|
+
if (!GetFileInformationByHandleEx(
|
|
500
|
+
handle,
|
|
501
|
+
FILE_REMOTE_PROTOCOL_INFO_CLASS,
|
|
502
|
+
buffer,
|
|
503
|
+
REMOTE_PROTOCOL_INFO_BUFFER_SIZE))
|
|
504
|
+
{
|
|
505
|
+
int error = Marshal.GetLastWin32Error();
|
|
506
|
+
if (error == ERROR_INVALID_PARAMETER) return;
|
|
507
|
+
throw new Win32Exception(error);
|
|
508
|
+
}
|
|
509
|
+
int structureSize = (ushort)Marshal.ReadInt16(buffer, 2);
|
|
510
|
+
uint protocol = unchecked((uint)Marshal.ReadInt32(buffer, 4));
|
|
511
|
+
if (structureSize < 20
|
|
512
|
+
|| structureSize > REMOTE_PROTOCOL_INFO_BUFFER_SIZE
|
|
513
|
+
|| protocol != 0)
|
|
514
|
+
{
|
|
515
|
+
throw new UnauthorizedAccessException();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
finally
|
|
519
|
+
{
|
|
520
|
+
Marshal.FreeHGlobal(buffer);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private static string NormalizeHandlePath(string path)
|
|
525
|
+
{
|
|
526
|
+
if (path.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase))
|
|
527
|
+
return @"\\" + path.Substring(8);
|
|
528
|
+
if (path.StartsWith(@"\\?\", StringComparison.OrdinalIgnoreCase))
|
|
529
|
+
return path.Substring(4);
|
|
530
|
+
return path;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private static void VerifyHandlePath(SafeFileHandle handle, string expectedPath)
|
|
534
|
+
{
|
|
535
|
+
StringBuilder buffer = new StringBuilder(32768);
|
|
536
|
+
uint length = GetFinalPathNameByHandleW(handle, buffer, (uint)buffer.Capacity, 0);
|
|
537
|
+
if (length == 0 || length >= buffer.Capacity)
|
|
538
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
539
|
+
string actual = Path.GetFullPath(NormalizeHandlePath(buffer.ToString()));
|
|
540
|
+
string expected = Path.GetFullPath(expectedPath);
|
|
541
|
+
if (!String.Equals(actual.TrimEnd('\\'), expected.TrimEnd('\\'),
|
|
542
|
+
StringComparison.OrdinalIgnoreCase))
|
|
543
|
+
{
|
|
544
|
+
throw new UnauthorizedAccessException();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private static SafeFileHandle OpenExisting(string target, bool directory, bool write)
|
|
549
|
+
{
|
|
550
|
+
uint access = GENERIC_READ | READ_CONTROL | WRITE_DAC | DELETE;
|
|
551
|
+
if (write) access |= GENERIC_WRITE;
|
|
552
|
+
uint flags = FILE_FLAG_OPEN_REPARSE_POINT;
|
|
553
|
+
if (directory) flags |= FILE_FLAG_BACKUP_SEMANTICS;
|
|
554
|
+
SafeFileHandle handle = CreateFileExisting(
|
|
555
|
+
target, access, 0, IntPtr.Zero, OPEN_EXISTING, flags, IntPtr.Zero);
|
|
556
|
+
if (handle.IsInvalid)
|
|
557
|
+
{
|
|
558
|
+
int error = Marshal.GetLastWin32Error();
|
|
559
|
+
handle.Dispose();
|
|
560
|
+
throw new Win32Exception(error);
|
|
561
|
+
}
|
|
562
|
+
return handle;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private static string NativePath(string target)
|
|
566
|
+
{
|
|
567
|
+
string full = Path.GetFullPath(target);
|
|
568
|
+
if (full.StartsWith(@"\\", StringComparison.Ordinal))
|
|
569
|
+
return @"\??\UNC\" + full.Substring(2);
|
|
570
|
+
return @"\??\" + full;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private static SafeFileHandle OpenOrCreateDirectory(
|
|
574
|
+
string target,
|
|
575
|
+
string userSid)
|
|
576
|
+
{
|
|
577
|
+
string nativePath = NativePath(target);
|
|
578
|
+
if (Encoding.Unicode.GetByteCount(nativePath) > UInt16.MaxValue - 2)
|
|
579
|
+
throw new PathTooLongException();
|
|
580
|
+
|
|
581
|
+
IntPtr nameBuffer = IntPtr.Zero;
|
|
582
|
+
IntPtr namePointer = IntPtr.Zero;
|
|
583
|
+
IntPtr descriptor = IntPtr.Zero;
|
|
584
|
+
IntPtr rawHandle = IntPtr.Zero;
|
|
585
|
+
try
|
|
586
|
+
{
|
|
587
|
+
nameBuffer = Marshal.StringToHGlobalUni(nativePath);
|
|
588
|
+
UnicodeString name = new UnicodeString();
|
|
589
|
+
name.Length = (ushort)Encoding.Unicode.GetByteCount(nativePath);
|
|
590
|
+
name.MaximumLength = (ushort)(name.Length + 2);
|
|
591
|
+
name.Buffer = nameBuffer;
|
|
592
|
+
namePointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(UnicodeString)));
|
|
593
|
+
Marshal.StructureToPtr(name, namePointer, false);
|
|
594
|
+
descriptor = CreateExactDescriptor(userSid, true);
|
|
595
|
+
|
|
596
|
+
ObjectAttributes attributes = new ObjectAttributes();
|
|
597
|
+
attributes.Length = Marshal.SizeOf(typeof(ObjectAttributes));
|
|
598
|
+
attributes.ObjectName = namePointer;
|
|
599
|
+
attributes.Attributes = OBJ_CASE_INSENSITIVE;
|
|
600
|
+
attributes.SecurityDescriptor = descriptor;
|
|
601
|
+
|
|
602
|
+
IoStatusBlock ioStatus;
|
|
603
|
+
int status = NtCreateFile(
|
|
604
|
+
out rawHandle,
|
|
605
|
+
GENERIC_READ | READ_CONTROL | WRITE_DAC | DELETE | SYNCHRONIZE,
|
|
606
|
+
ref attributes,
|
|
607
|
+
out ioStatus,
|
|
608
|
+
IntPtr.Zero,
|
|
609
|
+
0,
|
|
610
|
+
0,
|
|
611
|
+
FILE_OPEN_IF,
|
|
612
|
+
FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
|
|
613
|
+
IntPtr.Zero,
|
|
614
|
+
0);
|
|
615
|
+
if (status < 0)
|
|
616
|
+
throw new Win32Exception((int)RtlNtStatusToDosError(status));
|
|
617
|
+
return new SafeFileHandle(rawHandle, true);
|
|
618
|
+
}
|
|
619
|
+
catch
|
|
620
|
+
{
|
|
621
|
+
if (rawHandle != IntPtr.Zero && rawHandle != new IntPtr(-1))
|
|
622
|
+
new SafeFileHandle(rawHandle, true).Dispose();
|
|
623
|
+
throw;
|
|
624
|
+
}
|
|
625
|
+
finally
|
|
626
|
+
{
|
|
627
|
+
if (descriptor != IntPtr.Zero) LocalFree(descriptor);
|
|
628
|
+
if (namePointer != IntPtr.Zero) Marshal.FreeHGlobal(namePointer);
|
|
629
|
+
if (nameBuffer != IntPtr.Zero) Marshal.FreeHGlobal(nameBuffer);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private static void EnsureParentDirectories(string target, string userSid)
|
|
634
|
+
{
|
|
635
|
+
string parent = Path.GetDirectoryName(Path.GetFullPath(target));
|
|
636
|
+
if (String.IsNullOrEmpty(parent) || Directory.Exists(parent)) return;
|
|
637
|
+
EnsurePrivateDirectory(parent, userSid);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
public static void EnsurePrivateDirectory(string target, string userSid)
|
|
641
|
+
{
|
|
642
|
+
string fullTarget = Path.GetFullPath(target);
|
|
643
|
+
string root = Path.GetPathRoot(fullTarget);
|
|
644
|
+
if (String.Equals(fullTarget.TrimEnd('\\'), root.TrimEnd('\\'),
|
|
645
|
+
StringComparison.OrdinalIgnoreCase))
|
|
646
|
+
throw new UnauthorizedAccessException();
|
|
647
|
+
EnsureParentDirectories(fullTarget, userSid);
|
|
648
|
+
using (SafeFileHandle handle = OpenOrCreateDirectory(fullTarget, userSid))
|
|
649
|
+
{
|
|
650
|
+
ValidateHandle(handle, true);
|
|
651
|
+
VerifyHandlePath(handle, fullTarget);
|
|
652
|
+
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
653
|
+
string owner = ValidateOwner(handle, expectedSids);
|
|
654
|
+
SetExactDacl(handle, userSid, true);
|
|
655
|
+
VerifyExactDacl(handle, userSid, true, owner);
|
|
656
|
+
VerifyHandlePath(handle, fullTarget);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
public static void HardenExisting(string target, string userSid, bool directory)
|
|
661
|
+
{
|
|
662
|
+
using (SafeFileHandle handle = OpenExisting(target, directory, false))
|
|
663
|
+
{
|
|
664
|
+
ValidateHandle(handle, directory);
|
|
665
|
+
VerifyHandlePath(handle, target);
|
|
666
|
+
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
667
|
+
string owner = ValidateOwner(handle, expectedSids);
|
|
668
|
+
SetExactDacl(handle, userSid, directory);
|
|
669
|
+
VerifyExactDacl(handle, userSid, directory, owner);
|
|
670
|
+
ValidateHandle(handle, directory);
|
|
671
|
+
VerifyHandlePath(handle, target);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private static bool IsReservedTreeName(string name)
|
|
676
|
+
{
|
|
677
|
+
string stem = name;
|
|
678
|
+
int dot = stem.IndexOf('.');
|
|
679
|
+
if (dot >= 0) stem = stem.Substring(0, dot);
|
|
680
|
+
stem = stem.ToUpperInvariant();
|
|
681
|
+
if (stem == "CON" || stem == "PRN" || stem == "AUX" || stem == "NUL"
|
|
682
|
+
|| stem == "CONIN$" || stem == "CONOUT$") return true;
|
|
683
|
+
if (stem.Length == 4 && (stem.StartsWith("COM", StringComparison.Ordinal)
|
|
684
|
+
|| stem.StartsWith("LPT", StringComparison.Ordinal)))
|
|
685
|
+
{
|
|
686
|
+
char suffix = stem[3];
|
|
687
|
+
return suffix >= '1' && suffix <= '9';
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private static void ValidateTreeEntryName(string name)
|
|
693
|
+
{
|
|
694
|
+
if (String.IsNullOrEmpty(name) || name == "." || name == ".."
|
|
695
|
+
|| name.EndsWith(".", StringComparison.Ordinal)
|
|
696
|
+
|| name.EndsWith(" ", StringComparison.Ordinal)
|
|
697
|
+
|| IsReservedTreeName(name))
|
|
698
|
+
{
|
|
699
|
+
throw new UnauthorizedAccessException();
|
|
700
|
+
}
|
|
701
|
+
foreach (char character in name)
|
|
702
|
+
{
|
|
703
|
+
if (character < 32 || character == ':' || character == '\\' || character == '/'
|
|
704
|
+
|| character == '<' || character == '>' || character == '"'
|
|
705
|
+
|| character == '|' || character == '?' || character == '*')
|
|
706
|
+
{
|
|
707
|
+
throw new UnauthorizedAccessException();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private static string ValidateTreeRootPath(string target)
|
|
713
|
+
{
|
|
714
|
+
if (String.IsNullOrEmpty(target) || target.Length > MAX_TREE_PATH_LENGTH
|
|
715
|
+
|| target.Length < 3 || !Char.IsLetter(target[0]) || target[1] != ':'
|
|
716
|
+
|| target[2] != '\\' || target.IndexOf('/') >= 0
|
|
717
|
+
|| target.IndexOf(':', 2) >= 0)
|
|
718
|
+
{
|
|
719
|
+
throw new UnauthorizedAccessException();
|
|
720
|
+
}
|
|
721
|
+
string fullTarget = Path.GetFullPath(target);
|
|
722
|
+
if (!String.Equals(fullTarget, target, StringComparison.OrdinalIgnoreCase))
|
|
723
|
+
throw new UnauthorizedAccessException();
|
|
724
|
+
string root = Path.GetPathRoot(fullTarget);
|
|
725
|
+
if (String.Equals(fullTarget.TrimEnd('\\'), root.TrimEnd('\\'),
|
|
726
|
+
StringComparison.OrdinalIgnoreCase))
|
|
727
|
+
{
|
|
728
|
+
throw new UnauthorizedAccessException();
|
|
729
|
+
}
|
|
730
|
+
string relative = fullTarget.Substring(root.Length);
|
|
731
|
+
foreach (string segment in relative.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries))
|
|
732
|
+
ValidateTreeEntryName(segment);
|
|
733
|
+
return fullTarget;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private static string TreeChildPath(string root, string parent, string name)
|
|
737
|
+
{
|
|
738
|
+
ValidateTreeEntryName(name);
|
|
739
|
+
string child = Path.GetFullPath(Path.Combine(parent, name));
|
|
740
|
+
string prefix = root.TrimEnd('\\') + "\\";
|
|
741
|
+
if (child.Length > MAX_TREE_PATH_LENGTH
|
|
742
|
+
|| !child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
|
743
|
+
|| !String.Equals(Path.GetFileName(child), name, StringComparison.Ordinal))
|
|
744
|
+
{
|
|
745
|
+
throw new UnauthorizedAccessException();
|
|
746
|
+
}
|
|
747
|
+
return child;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private static List<TreeEntry> ReadDirectoryEntries(SafeFileHandle handle)
|
|
751
|
+
{
|
|
752
|
+
List<TreeEntry> entries = new List<TreeEntry>();
|
|
753
|
+
HashSet<string> names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
754
|
+
IntPtr buffer = Marshal.AllocHGlobal(DIRECTORY_INFORMATION_BUFFER_SIZE);
|
|
755
|
+
try
|
|
756
|
+
{
|
|
757
|
+
bool restartScan = true;
|
|
758
|
+
while (true)
|
|
759
|
+
{
|
|
760
|
+
IoStatusBlock ioStatus;
|
|
761
|
+
int status = NtQueryDirectoryFile(
|
|
762
|
+
handle,
|
|
763
|
+
IntPtr.Zero,
|
|
764
|
+
IntPtr.Zero,
|
|
765
|
+
IntPtr.Zero,
|
|
766
|
+
out ioStatus,
|
|
767
|
+
buffer,
|
|
768
|
+
DIRECTORY_INFORMATION_BUFFER_SIZE,
|
|
769
|
+
FILE_ID_BOTH_DIRECTORY_INFORMATION_CLASS,
|
|
770
|
+
true,
|
|
771
|
+
IntPtr.Zero,
|
|
772
|
+
restartScan);
|
|
773
|
+
restartScan = false;
|
|
774
|
+
if (status == STATUS_NO_MORE_FILES) break;
|
|
775
|
+
if (status != 0)
|
|
776
|
+
throw new Win32Exception((int)RtlNtStatusToDosError(status));
|
|
777
|
+
|
|
778
|
+
ulong bytesReturned = ioStatus.Information.ToUInt64();
|
|
779
|
+
if (bytesReturned < DIRECTORY_INFORMATION_NAME_OFFSET
|
|
780
|
+
|| bytesReturned > DIRECTORY_INFORMATION_BUFFER_SIZE)
|
|
781
|
+
{
|
|
782
|
+
throw new InvalidDataException();
|
|
783
|
+
}
|
|
784
|
+
int nameLength = Marshal.ReadInt32(buffer, 60);
|
|
785
|
+
if (nameLength < 0 || (nameLength & 1) != 0
|
|
786
|
+
|| (ulong)(DIRECTORY_INFORMATION_NAME_OFFSET + nameLength) > bytesReturned)
|
|
787
|
+
{
|
|
788
|
+
throw new InvalidDataException();
|
|
789
|
+
}
|
|
790
|
+
string name = Marshal.PtrToStringUni(
|
|
791
|
+
IntPtr.Add(buffer, DIRECTORY_INFORMATION_NAME_OFFSET),
|
|
792
|
+
nameLength / 2);
|
|
793
|
+
if (name == "." || name == "..") continue;
|
|
794
|
+
ValidateTreeEntryName(name);
|
|
795
|
+
if (!names.Add(name)) throw new UnauthorizedAccessException();
|
|
796
|
+
TreeEntry entry = new TreeEntry();
|
|
797
|
+
entry.Name = name;
|
|
798
|
+
entry.Attributes = unchecked((uint)Marshal.ReadInt32(buffer, 56));
|
|
799
|
+
entry.FileId = unchecked((ulong)Marshal.ReadInt64(
|
|
800
|
+
buffer,
|
|
801
|
+
DIRECTORY_INFORMATION_FILE_ID_OFFSET));
|
|
802
|
+
if (entry.FileId == 0) throw new UnauthorizedAccessException();
|
|
803
|
+
entries.Add(entry);
|
|
804
|
+
if (entries.Count > MAX_TREE_ENTRIES)
|
|
805
|
+
throw new UnauthorizedAccessException();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
finally
|
|
809
|
+
{
|
|
810
|
+
Marshal.FreeHGlobal(buffer);
|
|
811
|
+
}
|
|
812
|
+
entries.Sort(delegate(TreeEntry left, TreeEntry right)
|
|
813
|
+
{
|
|
814
|
+
int comparison = StringComparer.OrdinalIgnoreCase.Compare(left.Name, right.Name);
|
|
815
|
+
return comparison != 0 ? comparison : StringComparer.Ordinal.Compare(left.Name, right.Name);
|
|
816
|
+
});
|
|
817
|
+
return entries;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private static void VerifyUnchangedDirectoryEntries(
|
|
821
|
+
List<TreeEntry> expected,
|
|
822
|
+
List<TreeEntry> actual)
|
|
823
|
+
{
|
|
824
|
+
if (expected.Count != actual.Count) throw new UnauthorizedAccessException();
|
|
825
|
+
for (int index = 0; index < expected.Count; index++)
|
|
826
|
+
{
|
|
827
|
+
if (!String.Equals(expected[index].Name, actual[index].Name, StringComparison.Ordinal)
|
|
828
|
+
|| expected[index].Attributes != actual[index].Attributes
|
|
829
|
+
|| expected[index].FileId != actual[index].FileId)
|
|
830
|
+
{
|
|
831
|
+
throw new UnauthorizedAccessException();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private static string HardenTreeEntry(
|
|
837
|
+
SafeFileHandle handle,
|
|
838
|
+
string target,
|
|
839
|
+
string userSid,
|
|
840
|
+
bool directory,
|
|
841
|
+
HashSet<string> expectedSids)
|
|
842
|
+
{
|
|
843
|
+
ValidateHandle(handle, directory);
|
|
844
|
+
VerifyHandlePath(handle, target);
|
|
845
|
+
string owner = ValidateOwner(handle, expectedSids);
|
|
846
|
+
SetExactDacl(handle, userSid, directory);
|
|
847
|
+
VerifyExactDacl(handle, userSid, directory, owner);
|
|
848
|
+
ValidateHandle(handle, directory);
|
|
849
|
+
VerifyHandlePath(handle, target);
|
|
850
|
+
return owner;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private static void HardenTreeDirectory(
|
|
854
|
+
SafeFileHandle directoryHandle,
|
|
855
|
+
string root,
|
|
856
|
+
string directoryPath,
|
|
857
|
+
string userSid,
|
|
858
|
+
HashSet<string> expectedSids,
|
|
859
|
+
int depth,
|
|
860
|
+
ref int entryCount,
|
|
861
|
+
List<HeldTreeHandle> heldHandles)
|
|
862
|
+
{
|
|
863
|
+
if (depth > MAX_TREE_DEPTH) throw new UnauthorizedAccessException();
|
|
864
|
+
List<TreeEntry> snapshot = ReadDirectoryEntries(directoryHandle);
|
|
865
|
+
foreach (TreeEntry entry in snapshot)
|
|
866
|
+
{
|
|
867
|
+
entryCount++;
|
|
868
|
+
if (entryCount > MAX_TREE_ENTRIES) throw new UnauthorizedAccessException();
|
|
869
|
+
if ((entry.Attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE)) != 0)
|
|
870
|
+
throw new UnauthorizedAccessException();
|
|
871
|
+
bool directory = (entry.Attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
872
|
+
string childPath = TreeChildPath(root, directoryPath, entry.Name);
|
|
873
|
+
SafeFileHandle childHandle = OpenExisting(childPath, directory, false);
|
|
874
|
+
try
|
|
875
|
+
{
|
|
876
|
+
HeldTreeHandle heldHandle = new HeldTreeHandle();
|
|
877
|
+
heldHandle.Handle = childHandle;
|
|
878
|
+
heldHandle.Path = childPath;
|
|
879
|
+
heldHandle.Directory = directory;
|
|
880
|
+
heldHandles.Add(heldHandle);
|
|
881
|
+
}
|
|
882
|
+
catch
|
|
883
|
+
{
|
|
884
|
+
childHandle.Dispose();
|
|
885
|
+
throw;
|
|
886
|
+
}
|
|
887
|
+
VerifyTreeHandleIdentity(childHandle, entry.FileId);
|
|
888
|
+
heldHandles[heldHandles.Count - 1].Owner = HardenTreeEntry(
|
|
889
|
+
childHandle,
|
|
890
|
+
childPath,
|
|
891
|
+
userSid,
|
|
892
|
+
directory,
|
|
893
|
+
expectedSids);
|
|
894
|
+
if (directory)
|
|
895
|
+
{
|
|
896
|
+
HardenTreeDirectory(
|
|
897
|
+
childHandle,
|
|
898
|
+
root,
|
|
899
|
+
childPath,
|
|
900
|
+
userSid,
|
|
901
|
+
expectedSids,
|
|
902
|
+
depth + 1,
|
|
903
|
+
ref entryCount,
|
|
904
|
+
heldHandles);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
VerifyUnchangedDirectoryEntries(snapshot, ReadDirectoryEntries(directoryHandle));
|
|
908
|
+
ValidateHandle(directoryHandle, true);
|
|
909
|
+
VerifyHandlePath(directoryHandle, directoryPath);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
public static void HardenTree(string target, string userSid)
|
|
913
|
+
{
|
|
914
|
+
string root = ValidateTreeRootPath(target);
|
|
915
|
+
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
916
|
+
List<HeldTreeHandle> heldHandles = new List<HeldTreeHandle>();
|
|
917
|
+
try
|
|
918
|
+
{
|
|
919
|
+
SafeFileHandle rootHandle = OpenExisting(root, true, false);
|
|
920
|
+
HeldTreeHandle heldRoot = new HeldTreeHandle();
|
|
921
|
+
heldRoot.Handle = rootHandle;
|
|
922
|
+
heldRoot.Path = root;
|
|
923
|
+
heldRoot.Directory = true;
|
|
924
|
+
heldHandles.Add(heldRoot);
|
|
925
|
+
heldRoot.Owner = HardenTreeEntry(rootHandle, root, userSid, true, expectedSids);
|
|
926
|
+
int entryCount = 1;
|
|
927
|
+
HardenTreeDirectory(
|
|
928
|
+
rootHandle,
|
|
929
|
+
root,
|
|
930
|
+
root,
|
|
931
|
+
userSid,
|
|
932
|
+
expectedSids,
|
|
933
|
+
0,
|
|
934
|
+
ref entryCount,
|
|
935
|
+
heldHandles);
|
|
936
|
+
foreach (HeldTreeHandle heldHandle in heldHandles)
|
|
937
|
+
{
|
|
938
|
+
ValidateHandle(heldHandle.Handle, heldHandle.Directory);
|
|
939
|
+
VerifyExactDacl(
|
|
940
|
+
heldHandle.Handle,
|
|
941
|
+
userSid,
|
|
942
|
+
heldHandle.Directory,
|
|
943
|
+
heldHandle.Owner);
|
|
944
|
+
VerifyHandlePath(heldHandle.Handle, heldHandle.Path);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
finally
|
|
948
|
+
{
|
|
949
|
+
for (int index = heldHandles.Count - 1; index >= 0; index--)
|
|
950
|
+
heldHandles[index].Handle.Dispose();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
private static bool SetDeletePending(SafeFileHandle handle, bool deleteFile)
|
|
955
|
+
{
|
|
956
|
+
FileDispositionInfo information = new FileDispositionInfo();
|
|
957
|
+
information.DeleteFile = deleteFile;
|
|
958
|
+
return SetFileInformationByHandle(
|
|
959
|
+
handle,
|
|
960
|
+
FILE_DISPOSITION_INFO_CLASS,
|
|
961
|
+
ref information,
|
|
962
|
+
(uint)Marshal.SizeOf(typeof(FileDispositionInfo)));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private static SafeFileHandle OpenForWrite(string target, string userSid)
|
|
966
|
+
{
|
|
967
|
+
IntPtr descriptor = CreateExactDescriptor(userSid, false);
|
|
968
|
+
try
|
|
969
|
+
{
|
|
970
|
+
SecurityAttributes attributes = new SecurityAttributes();
|
|
971
|
+
attributes.Length = Marshal.SizeOf(typeof(SecurityAttributes));
|
|
972
|
+
attributes.SecurityDescriptor = descriptor;
|
|
973
|
+
attributes.InheritHandle = false;
|
|
974
|
+
SafeFileHandle handle = CreateFileNew(
|
|
975
|
+
target,
|
|
976
|
+
GENERIC_READ | GENERIC_WRITE | READ_CONTROL | WRITE_DAC | DELETE,
|
|
977
|
+
0,
|
|
978
|
+
ref attributes,
|
|
979
|
+
CREATE_NEW,
|
|
980
|
+
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
|
|
981
|
+
IntPtr.Zero);
|
|
982
|
+
if (!handle.IsInvalid) return handle;
|
|
983
|
+
|
|
984
|
+
int error = Marshal.GetLastWin32Error();
|
|
985
|
+
handle.Dispose();
|
|
986
|
+
throw new Win32Exception(error);
|
|
987
|
+
}
|
|
988
|
+
finally
|
|
989
|
+
{
|
|
990
|
+
LocalFree(descriptor);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
11
993
|
|
|
12
|
-
|
|
13
|
-
|
|
994
|
+
private static int FrameLength(byte[] frame)
|
|
995
|
+
{
|
|
996
|
+
if (frame == null || frame.Length < 8) throw new InvalidDataException();
|
|
997
|
+
ulong length = BitConverter.ToUInt64(frame, 0);
|
|
998
|
+
if (length > Int32.MaxValue || length != (ulong)(frame.Length - 8))
|
|
999
|
+
throw new InvalidDataException();
|
|
1000
|
+
return (int)length;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
public static void WritePrivateFile(string target, string userSid, byte[] frame)
|
|
1004
|
+
{
|
|
1005
|
+
int contentLength = FrameLength(frame);
|
|
1006
|
+
SafeFileHandle handle = OpenForWrite(target, userSid);
|
|
1007
|
+
FileStream stream = null;
|
|
1008
|
+
bool deletePending = false;
|
|
1009
|
+
try
|
|
1010
|
+
{
|
|
1011
|
+
ValidateHandle(handle, false);
|
|
1012
|
+
VerifyHandlePath(handle, target);
|
|
1013
|
+
if (!SetDeletePending(handle, true))
|
|
1014
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1015
|
+
deletePending = true;
|
|
1016
|
+
HashSet<string> expectedSids = ExpectedSids(userSid);
|
|
1017
|
+
string owner = ValidateOwner(handle, expectedSids);
|
|
1018
|
+
SetExactDacl(handle, userSid, false);
|
|
1019
|
+
VerifyExactDacl(handle, userSid, false, owner);
|
|
1020
|
+
|
|
1021
|
+
stream = new FileStream(handle, FileAccess.ReadWrite);
|
|
1022
|
+
stream.SetLength(0);
|
|
1023
|
+
stream.Write(frame, 8, contentLength);
|
|
1024
|
+
stream.Flush(true);
|
|
1025
|
+
VerifyFreshFileState(handle, true, false);
|
|
1026
|
+
VerifyExactDacl(handle, userSid, false, owner);
|
|
1027
|
+
VerifyHandlePath(handle, target);
|
|
1028
|
+
if (!SetDeletePending(handle, false))
|
|
1029
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1030
|
+
deletePending = false;
|
|
1031
|
+
ValidateHandle(handle, false);
|
|
1032
|
+
VerifyHandlePath(handle, target);
|
|
1033
|
+
}
|
|
1034
|
+
catch
|
|
1035
|
+
{
|
|
1036
|
+
try
|
|
1037
|
+
{
|
|
1038
|
+
if (stream != null)
|
|
1039
|
+
{
|
|
1040
|
+
stream.SetLength(0);
|
|
1041
|
+
stream.Flush(true);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
catch {}
|
|
1045
|
+
if (!deletePending)
|
|
1046
|
+
{
|
|
1047
|
+
try { deletePending = SetDeletePending(handle, true); }
|
|
1048
|
+
catch { deletePending = false; }
|
|
1049
|
+
}
|
|
1050
|
+
throw;
|
|
1051
|
+
}
|
|
1052
|
+
finally
|
|
1053
|
+
{
|
|
1054
|
+
Array.Clear(frame, 0, frame.Length);
|
|
1055
|
+
if (stream != null) stream.Dispose();
|
|
1056
|
+
else handle.Dispose();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}`;
|
|
1060
|
+
|
|
1061
|
+
const WINDOWS_ACL_CSHARP_GZIP = gzipSync(
|
|
1062
|
+
Buffer.from(WINDOWS_ACL_CSHARP),
|
|
1063
|
+
{ level: 9 },
|
|
1064
|
+
).toString('base64');
|
|
1065
|
+
|
|
1066
|
+
const WINDOWS_ACL_SCRIPT = [
|
|
1067
|
+
"$ErrorActionPreference = 'Stop'",
|
|
1068
|
+
"$ProgressPreference = 'SilentlyContinue'",
|
|
1069
|
+
'try {',
|
|
1070
|
+
'Set-StrictMode -Version 3',
|
|
1071
|
+
"$target=[Environment]::GetEnvironmentVariable('BLUN_PRIVATE_ACL_TARGET','Process')",
|
|
1072
|
+
"$kind=[Environment]::GetEnvironmentVariable('BLUN_PRIVATE_ACL_KIND','Process')",
|
|
1073
|
+
"$sid=[Environment]::GetEnvironmentVariable('BLUN_PRIVATE_ACL_USER_SID','Process')",
|
|
1074
|
+
"if ([string]::IsNullOrWhiteSpace($target) -or -not [IO.Path]::IsPathRooted($target)) { throw 'Invalid target.' }",
|
|
1075
|
+
"if ($kind -ne 'directory' -and $kind -ne 'file' -and $kind -ne 'file-write' -and $kind -ne 'tree') { throw 'Invalid kind.' }",
|
|
1076
|
+
`$bytes=[Convert]::FromBase64String('${WINDOWS_ACL_CSHARP_GZIP}')`,
|
|
1077
|
+
'$zipin=[IO.MemoryStream]::new($bytes)',
|
|
1078
|
+
'$gzip=[IO.Compression.GZipStream]::new($zipin,[IO.Compression.CompressionMode]::Decompress)',
|
|
1079
|
+
'$reader=[IO.StreamReader]::new($gzip,[Text.Encoding]::UTF8)',
|
|
1080
|
+
'try {',
|
|
1081
|
+
'$code=$reader.ReadToEnd()',
|
|
1082
|
+
'} finally {',
|
|
1083
|
+
'$reader.Dispose()',
|
|
1084
|
+
'$gzip.Dispose()',
|
|
1085
|
+
'$zipin.Dispose()',
|
|
1086
|
+
'[Array]::Clear($bytes,0,$bytes.Length)',
|
|
1087
|
+
'}',
|
|
1088
|
+
'$null=Add-Type -TypeDefinition $code -Language CSharp',
|
|
1089
|
+
"if ($kind -eq 'directory') {",
|
|
1090
|
+
'[BlunPrivateAclNative]::EnsurePrivateDirectory($target,$sid)',
|
|
1091
|
+
"} elseif ($kind -eq 'file') {",
|
|
1092
|
+
'[BlunPrivateAclNative]::HardenExisting($target,$sid,$false)',
|
|
1093
|
+
"} elseif ($kind -eq 'tree') {",
|
|
1094
|
+
'[BlunPrivateAclNative]::HardenTree($target,$sid)',
|
|
1095
|
+
'} else {',
|
|
1096
|
+
'$stdin=[Console]::OpenStandardInput()',
|
|
1097
|
+
'$mem=[IO.MemoryStream]::new()',
|
|
1098
|
+
'$frame=$null',
|
|
1099
|
+
'try {',
|
|
1100
|
+
'$stdin.CopyTo($mem)',
|
|
1101
|
+
'$frame=$mem.ToArray()',
|
|
1102
|
+
'[BlunPrivateAclNative]::WritePrivateFile($target,$sid,$frame)',
|
|
1103
|
+
'} finally {',
|
|
1104
|
+
'if ($null -ne $frame) { [Array]::Clear($frame,0,$frame.Length) }',
|
|
1105
|
+
'$mem.Dispose()',
|
|
1106
|
+
'}',
|
|
1107
|
+
'}',
|
|
1108
|
+
"[Console]::Out.Write('BLUN_PRIVATE_ACL_OK_V1')",
|
|
1109
|
+
'exit 0',
|
|
1110
|
+
'} catch {',
|
|
1111
|
+
"[Console]::Error.Write('BLUN_PRIVATE_ACL_ERROR_V1')",
|
|
1112
|
+
'exit 1',
|
|
1113
|
+
'}',
|
|
1114
|
+
].join('\n');
|
|
1115
|
+
|
|
1116
|
+
const WINDOWS_ACL_ENCODED_COMMAND = Buffer.from(WINDOWS_ACL_SCRIPT, 'utf16le').toString('base64');
|
|
1117
|
+
|
|
1118
|
+
function windowsSystemDirectory(options) {
|
|
1119
|
+
const env = options.env || process.env;
|
|
1120
|
+
const systemRoot = env.SystemRoot || env.SYSTEMROOT || env.WINDIR || env.windir;
|
|
1121
|
+
const directory = options.systemDirectory
|
|
1122
|
+
|| (systemRoot ? path.win32.join(systemRoot, 'System32') : '');
|
|
1123
|
+
if (!path.win32.isAbsolute(directory)
|
|
1124
|
+
|| path.win32.basename(directory).toLowerCase() !== 'system32') {
|
|
1125
|
+
throw new Error('Cannot resolve the absolute Windows system directory for private ACLs.');
|
|
1126
|
+
}
|
|
1127
|
+
return path.win32.normalize(directory);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function windowsHostEnvironment(systemDirectory) {
|
|
1131
|
+
const systemRoot = path.win32.dirname(systemDirectory);
|
|
1132
|
+
return { SystemRoot: systemRoot, WINDIR: systemRoot };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function runWindowsCommand(executable, args, options, env, input) {
|
|
1136
|
+
const execFileSyncImpl = options.execFileSyncImpl || execFileSync;
|
|
1137
|
+
const hasInput = input !== undefined;
|
|
1138
|
+
return execFileSyncImpl(executable, [...args], {
|
|
1139
|
+
encoding: 'utf8',
|
|
1140
|
+
env,
|
|
1141
|
+
input,
|
|
1142
|
+
maxBuffer: 64 * 1024,
|
|
1143
|
+
shell: false,
|
|
1144
|
+
stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
1145
|
+
timeout: 30_000,
|
|
1146
|
+
windowsHide: true,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function framedPrivateContents(contents) {
|
|
1151
|
+
const payload = Buffer.isBuffer(contents) ? Buffer.from(contents) : Buffer.from(String(contents));
|
|
1152
|
+
let frame;
|
|
1153
|
+
try {
|
|
1154
|
+
frame = Buffer.allocUnsafe(8 + payload.length);
|
|
1155
|
+
frame.writeBigUInt64LE(BigInt(payload.length), 0);
|
|
1156
|
+
payload.copy(frame, 8);
|
|
1157
|
+
return frame;
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
frame?.fill(0);
|
|
1160
|
+
throw error;
|
|
1161
|
+
} finally {
|
|
1162
|
+
payload.fill(0);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function resolveWindowsPrivateTarget(targetPath) {
|
|
1167
|
+
if (process.platform !== 'win32') return path.resolve(targetPath);
|
|
1168
|
+
try {
|
|
1169
|
+
if (typeof targetPath !== 'string'
|
|
1170
|
+
|| !WINDOWS_LOCAL_DOS_PATH_PATTERN.test(targetPath)
|
|
1171
|
+
|| !path.win32.isAbsolute(targetPath)
|
|
1172
|
+
|| targetPath.includes('/')
|
|
1173
|
+
|| targetPath.slice(2).includes(':')) {
|
|
1174
|
+
throw new Error('Invalid Windows private path.');
|
|
1175
|
+
}
|
|
1176
|
+
const normalizedTarget = path.win32.normalize(targetPath);
|
|
1177
|
+
if (normalizedTarget.toLowerCase() !== targetPath.toLowerCase()) {
|
|
1178
|
+
throw new Error('Invalid Windows private path.');
|
|
1179
|
+
}
|
|
1180
|
+
const segments = normalizedTarget.slice(3).split('\\').filter(Boolean);
|
|
1181
|
+
if (segments.some((segment) => segment.endsWith('.')
|
|
1182
|
+
|| segment.endsWith(' ')
|
|
1183
|
+
|| /[\u0000-\u001F<>"|?*]/u.test(segment)
|
|
1184
|
+
|| WINDOWS_RESERVED_PATH_SEGMENT.test(segment))) {
|
|
1185
|
+
throw new Error('Invalid Windows private path.');
|
|
1186
|
+
}
|
|
1187
|
+
const absoluteTarget = path.resolve(normalizedTarget);
|
|
1188
|
+
if (!WINDOWS_LOCAL_DOS_PATH_PATTERN.test(absoluteTarget)
|
|
1189
|
+
|| absoluteTarget.slice(2).includes(':')) {
|
|
1190
|
+
throw new Error('Invalid Windows private path.');
|
|
1191
|
+
}
|
|
1192
|
+
return absoluteTarget;
|
|
1193
|
+
} catch {
|
|
1194
|
+
throw new Error('Windows private ACL hardening or verification failed.');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function windowsAclController(options) {
|
|
1199
|
+
const systemDirectory = windowsSystemDirectory(options);
|
|
1200
|
+
const hostEnvironment = windowsHostEnvironment(systemDirectory);
|
|
1201
|
+
const whoami = path.win32.join(systemDirectory, 'whoami.exe');
|
|
1202
|
+
const powershell = path.win32.join(
|
|
1203
|
+
systemDirectory,
|
|
1204
|
+
'WindowsPowerShell',
|
|
1205
|
+
'v1.0',
|
|
1206
|
+
'powershell.exe',
|
|
1207
|
+
);
|
|
1208
|
+
let userSid;
|
|
1209
|
+
|
|
1210
|
+
const currentUserSid = () => {
|
|
1211
|
+
if (userSid !== undefined) return userSid;
|
|
1212
|
+
let output;
|
|
1213
|
+
try {
|
|
1214
|
+
output = runWindowsCommand(
|
|
1215
|
+
whoami,
|
|
1216
|
+
['/user', '/fo', 'csv', '/nh'],
|
|
1217
|
+
options,
|
|
1218
|
+
hostEnvironment,
|
|
1219
|
+
);
|
|
1220
|
+
} catch {
|
|
1221
|
+
throw new Error('Cannot determine the current Windows user SID for private launcher paths.');
|
|
1222
|
+
}
|
|
1223
|
+
const sids = [...new Set(String(output).match(WINDOWS_SID_PATTERN) || [])];
|
|
1224
|
+
if (sids.length !== 1) {
|
|
1225
|
+
throw new Error('Cannot determine the current Windows user SID for private launcher paths.');
|
|
1226
|
+
}
|
|
1227
|
+
userSid = sids[0].toUpperCase();
|
|
1228
|
+
return userSid;
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const invoke = (targetPath, kind, input) => {
|
|
1232
|
+
try {
|
|
1233
|
+
const absoluteTarget = resolveWindowsPrivateTarget(targetPath);
|
|
1234
|
+
const root = path.parse(absoluteTarget).root;
|
|
1235
|
+
if (!path.isAbsolute(absoluteTarget)
|
|
1236
|
+
|| root.toLowerCase() === absoluteTarget.toLowerCase()
|
|
1237
|
+
|| !['directory', 'file', 'file-write', 'tree'].includes(kind)) {
|
|
1238
|
+
throw new Error('Windows private ACL hardening or verification failed.');
|
|
1239
|
+
}
|
|
1240
|
+
const env = {
|
|
1241
|
+
...hostEnvironment,
|
|
1242
|
+
BLUN_PRIVATE_ACL_KIND: kind,
|
|
1243
|
+
BLUN_PRIVATE_ACL_TARGET: absoluteTarget,
|
|
1244
|
+
BLUN_PRIVATE_ACL_USER_SID: currentUserSid(),
|
|
1245
|
+
};
|
|
1246
|
+
let output;
|
|
1247
|
+
try {
|
|
1248
|
+
output = runWindowsCommand(
|
|
1249
|
+
powershell,
|
|
1250
|
+
['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand',
|
|
1251
|
+
WINDOWS_ACL_ENCODED_COMMAND],
|
|
1252
|
+
options,
|
|
1253
|
+
env,
|
|
1254
|
+
input,
|
|
1255
|
+
);
|
|
1256
|
+
} catch {
|
|
1257
|
+
throw new Error('Windows private ACL hardening or verification failed.');
|
|
1258
|
+
}
|
|
1259
|
+
if (output !== WINDOWS_ACL_OK_SENTINEL) {
|
|
1260
|
+
throw new Error('Windows private ACL hardening or verification failed.');
|
|
1261
|
+
}
|
|
1262
|
+
} finally {
|
|
1263
|
+
input?.fill(0);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
return {
|
|
1268
|
+
hardenAndVerify(targetPath, kind) {
|
|
1269
|
+
invoke(targetPath, kind);
|
|
1270
|
+
},
|
|
1271
|
+
hardenTree(targetPath) {
|
|
1272
|
+
invoke(targetPath, 'tree');
|
|
1273
|
+
},
|
|
1274
|
+
writeAndVerify(targetPath, contents) {
|
|
1275
|
+
invoke(targetPath, 'file-write', framedPrivateContents(contents));
|
|
1276
|
+
},
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function sameFileIdentity(left, right) {
|
|
1281
|
+
return left !== undefined
|
|
1282
|
+
&& right !== undefined
|
|
1283
|
+
&& left.dev !== undefined
|
|
1284
|
+
&& left.ino !== undefined
|
|
1285
|
+
&& left.dev === right.dev
|
|
1286
|
+
&& left.ino === right.ino;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function validatePosixEntry(stat, directory) {
|
|
1290
|
+
if (stat.isSymbolicLink()
|
|
1291
|
+
|| stat.isDirectory() !== directory
|
|
1292
|
+
|| stat.isFile() === directory
|
|
1293
|
+
|| (!directory && stat.nlink !== 1)) {
|
|
1294
|
+
throw new Error('Unsafe private path entry.');
|
|
1295
|
+
}
|
|
1296
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
1297
|
+
throw new Error('Unsafe private path owner.');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function verifyPosixHandlePath(targetPath, descriptor, identity, directory, fsImpl) {
|
|
1302
|
+
const byHandle = fsImpl.fstatSync(descriptor);
|
|
1303
|
+
const byPath = fsImpl.lstatSync(targetPath);
|
|
1304
|
+
validatePosixEntry(byHandle, directory);
|
|
1305
|
+
validatePosixEntry(byPath, directory);
|
|
1306
|
+
if (!sameFileIdentity(identity, byHandle)
|
|
1307
|
+
|| !sameFileIdentity(byHandle, byPath)) {
|
|
1308
|
+
throw new Error('Private path identity changed.');
|
|
1309
|
+
}
|
|
1310
|
+
return byHandle;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function openVerifiedPosixEntry(targetPath, fsImpl, requireDirectory = false) {
|
|
1314
|
+
const before = fsImpl.lstatSync(targetPath);
|
|
1315
|
+
const directory = before.isDirectory();
|
|
1316
|
+
validatePosixEntry(before, directory);
|
|
1317
|
+
if (requireDirectory && !directory) throw new Error('Private path is not a directory.');
|
|
1318
|
+
|
|
1319
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
1320
|
+
const directoryOnly = directory ? (fs.constants.O_DIRECTORY || 0) : 0;
|
|
1321
|
+
let descriptor;
|
|
1322
|
+
try {
|
|
1323
|
+
descriptor = fsImpl.openSync(
|
|
1324
|
+
targetPath,
|
|
1325
|
+
fs.constants.O_RDONLY | noFollow | directoryOnly,
|
|
1326
|
+
);
|
|
1327
|
+
verifyPosixHandlePath(targetPath, descriptor, before, directory, fsImpl);
|
|
1328
|
+
return { descriptor, directory, identity: before };
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (descriptor !== undefined) {
|
|
1331
|
+
try {
|
|
1332
|
+
fsImpl.closeSync(descriptor);
|
|
1333
|
+
} catch {
|
|
1334
|
+
// Preserve the verification failure.
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function applyPosixModeByHandle(
|
|
1342
|
+
targetPath,
|
|
1343
|
+
descriptor,
|
|
1344
|
+
identity,
|
|
1345
|
+
directory,
|
|
1346
|
+
mode,
|
|
1347
|
+
fsImpl,
|
|
1348
|
+
options,
|
|
1349
|
+
) {
|
|
1350
|
+
verifyPosixHandlePath(targetPath, descriptor, identity, directory, fsImpl);
|
|
1351
|
+
const emulatingPosixOnWindows = process.platform === 'win32'
|
|
1352
|
+
&& options.platform !== 'win32';
|
|
1353
|
+
if (!emulatingPosixOnWindows) fsImpl.fchmodSync(descriptor, mode);
|
|
1354
|
+
const hardened = verifyPosixHandlePath(
|
|
1355
|
+
targetPath,
|
|
1356
|
+
descriptor,
|
|
1357
|
+
identity,
|
|
1358
|
+
directory,
|
|
1359
|
+
fsImpl,
|
|
1360
|
+
);
|
|
1361
|
+
if (!emulatingPosixOnWindows && (hardened.mode & 0o777) !== mode) {
|
|
1362
|
+
throw new Error('Private path mode verification failed.');
|
|
14
1363
|
}
|
|
15
1364
|
}
|
|
16
1365
|
|
|
17
1366
|
function ensurePrivateDirectory(targetPath, options = {}) {
|
|
18
1367
|
const fsImpl = options.fsImpl || fs;
|
|
19
|
-
|
|
20
|
-
|
|
1368
|
+
const platform = options.platform || process.platform;
|
|
1369
|
+
if (platform === 'win32') {
|
|
1370
|
+
windowsAclController(options).hardenAndVerify(targetPath, 'directory');
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
1375
|
+
let opened;
|
|
1376
|
+
try {
|
|
1377
|
+
fsImpl.mkdirSync(absoluteTarget, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
1378
|
+
opened = openVerifiedPosixEntry(absoluteTarget, fsImpl, true);
|
|
1379
|
+
applyPosixModeByHandle(
|
|
1380
|
+
absoluteTarget,
|
|
1381
|
+
opened.descriptor,
|
|
1382
|
+
opened.identity,
|
|
1383
|
+
true,
|
|
1384
|
+
PRIVATE_DIRECTORY_MODE,
|
|
1385
|
+
fsImpl,
|
|
1386
|
+
options,
|
|
1387
|
+
);
|
|
1388
|
+
} catch {
|
|
1389
|
+
throw new Error('Private directory hardening or verification failed.');
|
|
1390
|
+
} finally {
|
|
1391
|
+
if (opened !== undefined) fsImpl.closeSync(opened.descriptor);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function scrubCreatedPosixFile(targetPath, descriptor, identity, fsImpl) {
|
|
1396
|
+
try {
|
|
1397
|
+
fsImpl.ftruncateSync(descriptor, 0);
|
|
1398
|
+
fsImpl.fsyncSync(descriptor);
|
|
1399
|
+
} catch {
|
|
1400
|
+
// Continue with identity-bound removal even if scrubbing was incomplete.
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
const byHandle = fsImpl.fstatSync(descriptor);
|
|
1404
|
+
const byPath = fsImpl.lstatSync(targetPath);
|
|
1405
|
+
if (sameFileIdentity(identity, byHandle)
|
|
1406
|
+
&& sameFileIdentity(byHandle, byPath)
|
|
1407
|
+
&& !byPath.isSymbolicLink()
|
|
1408
|
+
&& byPath.isFile()) {
|
|
1409
|
+
fsImpl.unlinkSync(targetPath);
|
|
1410
|
+
}
|
|
1411
|
+
} catch {
|
|
1412
|
+
// Never delete a pathname whose identity cannot be proven.
|
|
1413
|
+
}
|
|
21
1414
|
}
|
|
22
1415
|
|
|
23
1416
|
function writePrivateFile(targetPath, contents, options = {}) {
|
|
24
1417
|
const fsImpl = options.fsImpl || fs;
|
|
25
|
-
|
|
26
|
-
|
|
1418
|
+
const platform = options.platform || process.platform;
|
|
1419
|
+
if (platform === 'win32') {
|
|
1420
|
+
windowsAclController(options).writeAndVerify(targetPath, contents);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
1425
|
+
const payload = Buffer.isBuffer(contents) ? Buffer.from(contents) : Buffer.from(String(contents));
|
|
1426
|
+
let descriptor;
|
|
1427
|
+
let identity;
|
|
1428
|
+
let completed = false;
|
|
1429
|
+
try {
|
|
1430
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
1431
|
+
descriptor = fsImpl.openSync(
|
|
1432
|
+
absoluteTarget,
|
|
1433
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
|
|
1434
|
+
PRIVATE_FILE_MODE,
|
|
1435
|
+
);
|
|
1436
|
+
identity = fsImpl.fstatSync(descriptor);
|
|
1437
|
+
validatePosixEntry(identity, false);
|
|
1438
|
+
if (identity.size > 0) throw new Error('New private file is not empty.');
|
|
1439
|
+
verifyPosixHandlePath(absoluteTarget, descriptor, identity, false, fsImpl);
|
|
1440
|
+
applyPosixModeByHandle(
|
|
1441
|
+
absoluteTarget,
|
|
1442
|
+
descriptor,
|
|
1443
|
+
identity,
|
|
1444
|
+
false,
|
|
1445
|
+
PRIVATE_FILE_MODE,
|
|
1446
|
+
fsImpl,
|
|
1447
|
+
options,
|
|
1448
|
+
);
|
|
1449
|
+
fsImpl.writeFileSync(descriptor, payload);
|
|
1450
|
+
fsImpl.fsyncSync(descriptor);
|
|
1451
|
+
verifyPosixHandlePath(absoluteTarget, descriptor, identity, false, fsImpl);
|
|
1452
|
+
completed = true;
|
|
1453
|
+
} catch {
|
|
1454
|
+
throw new Error('Private file hardening or verification failed.');
|
|
1455
|
+
} finally {
|
|
1456
|
+
if (descriptor !== undefined) {
|
|
1457
|
+
if (!completed && identity !== undefined) {
|
|
1458
|
+
scrubCreatedPosixFile(absoluteTarget, descriptor, identity, fsImpl);
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
fsImpl.closeSync(descriptor);
|
|
1462
|
+
} catch {
|
|
1463
|
+
completed = false;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
payload.fill(0);
|
|
1467
|
+
}
|
|
1468
|
+
if (!completed) throw new Error('Private file hardening or verification failed.');
|
|
27
1469
|
}
|
|
28
1470
|
|
|
29
1471
|
function securePrivateFile(targetPath, options = {}) {
|
|
30
|
-
|
|
1472
|
+
const fsImpl = options.fsImpl || fs;
|
|
1473
|
+
const platform = options.platform || process.platform;
|
|
1474
|
+
if (platform === 'win32') {
|
|
1475
|
+
windowsAclController(options).hardenAndVerify(targetPath, 'file');
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
1480
|
+
let opened;
|
|
1481
|
+
try {
|
|
1482
|
+
opened = openVerifiedPosixEntry(absoluteTarget, fsImpl);
|
|
1483
|
+
if (opened.directory) throw new Error('Private path is not a file.');
|
|
1484
|
+
applyPosixModeByHandle(
|
|
1485
|
+
absoluteTarget,
|
|
1486
|
+
opened.descriptor,
|
|
1487
|
+
opened.identity,
|
|
1488
|
+
false,
|
|
1489
|
+
PRIVATE_FILE_MODE,
|
|
1490
|
+
fsImpl,
|
|
1491
|
+
options,
|
|
1492
|
+
);
|
|
1493
|
+
} catch {
|
|
1494
|
+
throw new Error('Private file hardening or verification failed.');
|
|
1495
|
+
} finally {
|
|
1496
|
+
if (opened !== undefined) fsImpl.closeSync(opened.descriptor);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function securePosixPrivateTree(targetPath, fsImpl, options) {
|
|
1501
|
+
const root = path.resolve(targetPath);
|
|
1502
|
+
let entryCount = 0;
|
|
1503
|
+
|
|
1504
|
+
const visit = (currentPath, depth, rootEntry, parent) => {
|
|
1505
|
+
if (depth > PRIVATE_TREE_MAX_DEPTH
|
|
1506
|
+
|| currentPath.length > PRIVATE_TREE_MAX_PATH_LENGTH
|
|
1507
|
+
|| ++entryCount > PRIVATE_TREE_MAX_ENTRIES) {
|
|
1508
|
+
throw new Error('Private tree hardening or verification failed.');
|
|
1509
|
+
}
|
|
1510
|
+
const opened = openVerifiedPosixEntry(currentPath, fsImpl, rootEntry);
|
|
1511
|
+
try {
|
|
1512
|
+
if (parent !== undefined) {
|
|
1513
|
+
verifyPosixHandlePath(
|
|
1514
|
+
parent.path,
|
|
1515
|
+
parent.descriptor,
|
|
1516
|
+
parent.identity,
|
|
1517
|
+
true,
|
|
1518
|
+
fsImpl,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
applyPosixModeByHandle(
|
|
1522
|
+
currentPath,
|
|
1523
|
+
opened.descriptor,
|
|
1524
|
+
opened.identity,
|
|
1525
|
+
opened.directory,
|
|
1526
|
+
opened.directory ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE,
|
|
1527
|
+
fsImpl,
|
|
1528
|
+
options,
|
|
1529
|
+
);
|
|
1530
|
+
|
|
1531
|
+
if (opened.directory) {
|
|
1532
|
+
verifyPosixHandlePath(
|
|
1533
|
+
currentPath,
|
|
1534
|
+
opened.descriptor,
|
|
1535
|
+
opened.identity,
|
|
1536
|
+
true,
|
|
1537
|
+
fsImpl,
|
|
1538
|
+
);
|
|
1539
|
+
const names = fsImpl.readdirSync(currentPath).toSorted();
|
|
1540
|
+
verifyPosixHandlePath(
|
|
1541
|
+
currentPath,
|
|
1542
|
+
opened.descriptor,
|
|
1543
|
+
opened.identity,
|
|
1544
|
+
true,
|
|
1545
|
+
fsImpl,
|
|
1546
|
+
);
|
|
1547
|
+
for (const name of names) {
|
|
1548
|
+
if (name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
|
|
1549
|
+
throw new Error('Private tree hardening or verification failed.');
|
|
1550
|
+
}
|
|
1551
|
+
verifyPosixHandlePath(
|
|
1552
|
+
currentPath,
|
|
1553
|
+
opened.descriptor,
|
|
1554
|
+
opened.identity,
|
|
1555
|
+
true,
|
|
1556
|
+
fsImpl,
|
|
1557
|
+
);
|
|
1558
|
+
visit(path.join(currentPath, name), depth + 1, false, {
|
|
1559
|
+
descriptor: opened.descriptor,
|
|
1560
|
+
identity: opened.identity,
|
|
1561
|
+
path: currentPath,
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
const afterNames = fsImpl.readdirSync(currentPath).toSorted();
|
|
1565
|
+
if (names.length !== afterNames.length
|
|
1566
|
+
|| names.some((name, index) => name !== afterNames[index])) {
|
|
1567
|
+
throw new Error('Private tree hardening or verification failed.');
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
verifyPosixHandlePath(
|
|
1571
|
+
currentPath,
|
|
1572
|
+
opened.descriptor,
|
|
1573
|
+
opened.identity,
|
|
1574
|
+
opened.directory,
|
|
1575
|
+
fsImpl,
|
|
1576
|
+
);
|
|
1577
|
+
} finally {
|
|
1578
|
+
fsImpl.closeSync(opened.descriptor);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
try {
|
|
1583
|
+
visit(root, 0, true);
|
|
1584
|
+
} catch {
|
|
1585
|
+
throw new Error('Private tree hardening or verification failed.');
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function securePrivateTree(targetPath, options = {}) {
|
|
1590
|
+
const fsImpl = options.fsImpl || fs;
|
|
1591
|
+
const platform = options.platform || process.platform;
|
|
1592
|
+
if (platform === 'win32') {
|
|
1593
|
+
windowsAclController(options).hardenTree(targetPath);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
securePosixPrivateTree(targetPath, fsImpl, options);
|
|
31
1597
|
}
|
|
32
1598
|
|
|
33
1599
|
module.exports = {
|
|
@@ -35,5 +1601,6 @@ module.exports = {
|
|
|
35
1601
|
PRIVATE_FILE_MODE,
|
|
36
1602
|
ensurePrivateDirectory,
|
|
37
1603
|
securePrivateFile,
|
|
1604
|
+
securePrivateTree,
|
|
38
1605
|
writePrivateFile,
|
|
39
1606
|
};
|