mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Credit the fix in CHANGELOG and tidy the wording of its inline comment. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
223 lines
8.2 KiB
JavaScript
223 lines
8.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
commandCompletedSuccessfully,
|
|
parseWindowsUserSid,
|
|
} from '../../src/keySetupCore.mjs';
|
|
|
|
/** PowerShell verification for the exact owner-only Windows credential DACL. */
|
|
const WINDOWS_ACL_VERIFY_SCRIPT = [
|
|
"$ErrorActionPreference = 'Stop'",
|
|
// Load Microsoft.PowerShell.Security (Get-Acl) from the module tree of the
|
|
// interpreter that is actually running. $PSHOME is that interpreter's own
|
|
// physical directory, so this is correct even when the executable was named
|
|
// through the Sysnative bridge, and it cannot be steered by anything the
|
|
// parent environment set.
|
|
"$env:PSModulePath = Join-Path $PSHOME 'Modules'",
|
|
'$acl = Get-Acl -LiteralPath $env:GEV_ACL_FILE',
|
|
'if (-not $acl.AreAccessRulesProtected) { exit 2 }',
|
|
"$allowed = @($env:GEV_ACL_USER_SID, 'S-1-5-18', 'S-1-5-32-544')",
|
|
'$seen = @{}',
|
|
'$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))',
|
|
'if ($rules.Count -ne 3) { exit 7 }',
|
|
'foreach ($rule in $rules) {',
|
|
' $ruleSid = $rule.IdentityReference.Value',
|
|
' if ($rule.IsInherited) { exit 3 }',
|
|
' if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { exit 4 }',
|
|
' if ($allowed -notcontains $ruleSid) { exit 5 }',
|
|
' if ($seen.ContainsKey($ruleSid)) { exit 8 }',
|
|
' $full = [System.Security.AccessControl.FileSystemRights]::FullControl',
|
|
' if ($rule.FileSystemRights -ne $full) { exit 6 }',
|
|
' $seen[$ruleSid] = $true',
|
|
'}',
|
|
'if ($seen.Count -ne 3) { exit 9 }',
|
|
].join('; ');
|
|
|
|
/**
|
|
* Resolve the native Windows ACL tools without consulting PATH.
|
|
*
|
|
* Provider Settings supports the standard Windows installation layout only:
|
|
* a local drive root named `Windows` (for example C:\\Windows or D:\\Windows).
|
|
* Requiring consistent aliases, canonical paths, and regular files prevents an
|
|
* inherited environment override, UNC share, device path, junction, or PATH
|
|
* shim from being treated as an operating-system security tool.
|
|
*/
|
|
function resolveWindowsNativeTools(environment, fileSystem, architecture) {
|
|
const aliases = ['SystemRoot', 'SYSTEMROOT', 'WINDIR', 'windir'];
|
|
const configured = aliases
|
|
.map((name) => environment[name])
|
|
.filter((value) => typeof value === 'string' && value.length > 0);
|
|
if (configured.length === 0) return null;
|
|
|
|
const roots = configured.map((value) => {
|
|
if (value !== value.trim() || !/^[A-Za-z]:\\Windows\\?$/i.test(value))
|
|
return null;
|
|
return value.endsWith('\\') ? value.slice(0, -1) : value;
|
|
});
|
|
if (roots.some((root) => !root)) return null;
|
|
if (roots.some((root) => root.toLowerCase() !== roots[0].toLowerCase()))
|
|
return null;
|
|
|
|
const systemRoot = roots[0];
|
|
const systemDirectory = architecture === 'ia32' ? 'Sysnative' : 'System32';
|
|
const expected = {
|
|
whoami: path.win32.join(systemRoot, systemDirectory, 'whoami.exe'),
|
|
icacls: path.win32.join(systemRoot, systemDirectory, 'icacls.exe'),
|
|
powershell: path.win32.join(
|
|
systemRoot,
|
|
systemDirectory,
|
|
'WindowsPowerShell',
|
|
'v1.0',
|
|
'powershell.exe',
|
|
),
|
|
};
|
|
|
|
try {
|
|
const realpath = fileSystem.realpathSync.native || fileSystem.realpathSync;
|
|
const rootEntry = fileSystem.lstatSync(systemRoot);
|
|
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) return null;
|
|
const canonicalRoot = realpath.call(fileSystem.realpathSync, systemRoot);
|
|
if (canonicalRoot.toLowerCase() !== systemRoot.toLowerCase()) return null;
|
|
for (const executable of Object.values(expected)) {
|
|
const entry = fileSystem.lstatSync(executable);
|
|
if (!entry.isFile() || entry.isSymbolicLink()) return null;
|
|
const canonicalExecutable = realpath.call(
|
|
fileSystem.realpathSync,
|
|
executable,
|
|
);
|
|
const canonicalCandidates = [executable];
|
|
if (architecture === 'ia32') {
|
|
canonicalCandidates.push(
|
|
executable.replace('\\Sysnative\\', '\\System32\\'),
|
|
);
|
|
}
|
|
if (
|
|
!canonicalCandidates.some(
|
|
(candidate) =>
|
|
candidate.toLowerCase() === canonicalExecutable.toLowerCase(),
|
|
)
|
|
)
|
|
return null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return expected;
|
|
}
|
|
|
|
/**
|
|
* Restrict a credential file before any secret is written to it.
|
|
* Dependencies are injectable so every fail-closed branch is unit-testable.
|
|
*/
|
|
export function hardenCredentialFile(
|
|
filepath,
|
|
{
|
|
platform = process.platform,
|
|
architecture = process.arch,
|
|
spawn = spawnSync,
|
|
fileSystem = fs,
|
|
environment = process.env,
|
|
} = {},
|
|
) {
|
|
if (platform !== 'win32') {
|
|
try {
|
|
if (platform === 'darwin') {
|
|
// `-N` (strip the ACL) is Apple-only. Spawn /bin/chmod by absolute
|
|
// path, never by name: a Nix/Homebrew coreutils profile puts GNU chmod
|
|
// first on PATH, and GNU chmod rejects `-N` — which was treated as a
|
|
// hardening failure and refused every save on such machines.
|
|
const aclRemoval = spawn('/bin/chmod', ['-N', filepath], {
|
|
stdio: 'ignore',
|
|
});
|
|
if (!commandCompletedSuccessfully(aclRemoval)) return false;
|
|
}
|
|
fileSystem.chmodSync(filepath, 0o600);
|
|
return (fileSystem.statSync(filepath).mode & 0o777) === 0o600;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const tools = resolveWindowsNativeTools(
|
|
environment,
|
|
fileSystem,
|
|
architecture,
|
|
);
|
|
if (!tools) return false;
|
|
|
|
try {
|
|
// Grant by the CURRENT PROCESS TOKEN'S SID, never a bare username. Parsing
|
|
// the second CSV field structurally prevents an SID-looking account name or
|
|
// a broad group SID from becoming the credential owner.
|
|
const whoami = spawn(tools.whoami, ['/user', '/fo', 'csv', '/nh'], {
|
|
encoding: 'utf8',
|
|
windowsHide: true,
|
|
});
|
|
const sid = commandCompletedSuccessfully(whoami)
|
|
? parseWindowsUserSid(whoami.stdout)
|
|
: null;
|
|
if (!sid) return false;
|
|
|
|
const applied = spawn(
|
|
tools.icacls,
|
|
[
|
|
filepath,
|
|
'/inheritance:r',
|
|
'/grant:r',
|
|
`*${sid}:F`,
|
|
'*S-1-5-18:F',
|
|
'*S-1-5-32-544:F',
|
|
],
|
|
{ stdio: 'ignore', windowsHide: true },
|
|
);
|
|
if (!commandCompletedSuccessfully(applied)) return false;
|
|
|
|
// Command success is not proof of the resulting DACL. Query it back and
|
|
// accept only three explicit FullControl allow principals, with inheritance
|
|
// disabled. Any unexpected rule, right, command error, or missing principal
|
|
// fails closed before the secret reaches disk.
|
|
//
|
|
// The verify process must load Microsoft.PowerShell.Security (Get-Acl)
|
|
// from the Windows PowerShell system module tree ONLY. A side-by-side
|
|
// PowerShell 7 install prepends its own module trees to PSModulePath at
|
|
// startup; inherited into a 5.1 process, the incompatible 7.x manifest
|
|
// cannot be autoloaded and the verify step fails. The script itself sets
|
|
// the path from $PSHOME; this is the same value computed ahead of time, so
|
|
// nothing inherited is in force even for the moment before it runs. The
|
|
// Sysnative spelling is a 32-bit caller's bridge and not a directory the
|
|
// launched native process can read, so the physical name is used.
|
|
const powershellModuleDirectory = path.win32.join(
|
|
path.win32
|
|
.dirname(tools.powershell)
|
|
.replace(/\\Sysnative\\/i, '\\System32\\'),
|
|
'Modules',
|
|
);
|
|
// Windows environment names are case-insensitive, and a child can end up
|
|
// carrying a differently cased alias alongside the value set here. Drop
|
|
// every spelling before setting the trusted one.
|
|
const verifyEnvironment = Object.fromEntries(
|
|
Object.entries(environment).filter(
|
|
([name]) => name.toLowerCase() !== 'psmodulepath',
|
|
),
|
|
);
|
|
const verified = spawn(
|
|
tools.powershell,
|
|
['-NoProfile', '-NonInteractive', '-Command', WINDOWS_ACL_VERIFY_SCRIPT],
|
|
{
|
|
env: {
|
|
...verifyEnvironment,
|
|
GEV_ACL_FILE: filepath,
|
|
GEV_ACL_USER_SID: sid,
|
|
PSModulePath: powershellModuleDirectory,
|
|
},
|
|
stdio: 'ignore',
|
|
windowsHide: true,
|
|
},
|
|
);
|
|
return commandCompletedSuccessfully(verified);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|