OmadaWeb.PS is used by identity administrators to authenticate against Omada environments, so it handles session cookies, credentials and OAuth2 tokens. Security reports are taken seriously and are handled privately.
Only the most recent version published to the PowerShell Gallery receives security fixes. Fixes are shipped as a new release rather than as patches to older versions.
| Version | Supported |
|---|---|
| Latest release on the PowerShell Gallery | ✅ |
| Any earlier release | ❌ |
Run Update-Module -Name OmadaWeb.PS to move to the supported version.
Do not open a public GitHub issue for a security problem.
Report privately through GitHub Security Advisories:
- Go to https://github.com/Fortigi/OmadaWeb.PS/security/advisories/new
- Describe the issue and submit the draft advisory.
The report is visible only to the maintainers until an advisory is published. If GitHub is not
available to you, email devops@fortigi.nl with OmadaWeb.PS security in the subject line.
- The module version (
Get-Module OmadaWeb.PS -ListAvailable) and PowerShell edition/version ($PSVersionTable). - The
-AuthenticationTypein use and whether the report involves cached cookies, browser profiles or the runtime-downloaded binaries. - Steps to reproduce, ideally as a minimal command sequence.
- The impact you believe the issue has.
Please redact real tokens, cookies and credentials from anything you send.
| Stage | Target |
|---|---|
| Acknowledgement of your report | 3 business days |
| Initial assessment and severity classification | 10 business days |
| Fix or documented mitigation for high/critical findings | 30 calendar days after assessment |
| Public advisory | Published together with the fix release |
If a fix takes longer than these targets, you will be told why and given a revised date. Reporters are credited in the advisory unless they ask not to be.
We ask that you give us a reasonable opportunity to ship a fix before disclosing publicly, and that testing is limited to environments you own or are authorized to test.
In scope:
- The PowerShell code in this repository.
- How the module stores and protects authentication material on disk (see Local data footprint in the README).
- How the module downloads, verifies and loads its runtime dependencies.
Out of scope:
- The Omada product itself. Report those to Omada directly.
- Vulnerabilities in third-party components (Selenium, Newtonsoft.Json, WebView2, msedgedriver). Report those to their maintainers; if the module ships an unsafe version or configuration of one, that part is in scope, so tell us as well.
- Findings that require an attacker to already have interactive access to the user's Windows account under which the module runs.
| Control | Where it is configured |
|---|---|
| Private vulnerability reporting | Repository settings — Settings > Advanced Security > Private vulnerability reporting |
| Secret scanning and push protection | Repository settings — Settings > Advanced Security |
| Dependabot version and security updates | .github/dependabot.yml |
| Automatic validation of Dependabot pull requests gated on update type, release age and the OpenSSF Scorecard of the source repository — merging stays manual | .github/workflows/dependabot-validation.yml, policy in Build/DependabotValidation.psd1, see below |
Every GitHub Actions uses: pinned to a full commit SHA, with a # vX.Y.Z comment Dependabot keeps current |
every workflow in .github/workflows, enforced by Tests/Unit/WorkflowActionPins.Tests.ps1 |
| Integrity verification of runtime-downloaded binaries | OmadaWeb.PS/DependencyLock.psd1, see below |
| Software Bill of Materials (CycloneDX) | Generated per release, see below |
The Microsoft WebView2 assemblies ship inside the module, under
lib\<edition>\<architecture> — see Why the WebView2 assemblies are bundled.
Selenium's WebDriver.dll, Newtonsoft.Json.dll, System.Text.Json.dll and the dependencies it
needs on .NET Framework, System.Runtime.dll and msedgedriver.exe are downloaded to
%LOCALAPPDATA%\OmadaWeb.PS\Bin the first time they are needed, and then loaded into the PowerShell
session with Add-Type / Assembly.LoadFrom, or executed. A module built or installed without its
bundle downloads the WebView2 assemblies the same way.
That makes those downloads the module's entire binary supply chain, so each one is verified before it
is expanded, copied into Bin or loaded:
- Pinned and hash-verified. Every downloadable artefact has a pinned version, an exact download
URL and an expected SHA-256 in
OmadaWeb.PS/DependencyLock.psd1, which ships with the module.Invoke-DownloadFilechecks the downloaded bytes against that hash and, on a mismatch, deletes the file and aborts with an error naming the artefact and both hashes. - Fail closed. An artefact with no entry in the lock file is not downloaded at all, and a hash-pinned artefact cannot be fetched from any URL other than its pinned one. A missing or unreadable lock file stops every download rather than allowing an unverified one.
msedgedriver.exeis verified by signature. Its version has to match the Microsoft Edge build installed on the machine, so it cannot be pinned or hashed ahead of time. Its Authenticode signature is checked instead — the signature must be valid and issued toO=Microsoft Corporation— before the executable is moved into place.
Pinning would be a liability if nobody noticed a pin going stale or turning vulnerable, so the pinned
packages are declared as PackageReference items in Build/Dependencies. That
puts them in this repository's dependency graph, which is what makes Dependabot alerts and
security-update pull requests possible for components that are never restored from a package
manifest.
A bump touches three files, not one. The version lives in Build/Dependencies, the version and
SHA-256 live in the lock file, and the SBOM inventory in Build/Dependencies.psd1 reports the version
that is actually loaded. The flow after a bump — whether Dependabot proposes it or a maintainer does
— is:
- the version changes in
Build/Dependencies; .github/workflows/dependency-lock-sync.ymlrefreshes the pinned hash and the SBOM version on that branch. It sweeps every open Dependabot pull request weekly, is started by Dependabot validation (below) for an eligible bump, and can be run from the Actions tab against a single branch — after a rebase, say. It checks out two trees and runs the script from the default branch against the pull request branch as data, so nothing from the branch under review executes with a write-scoped token. It commits through the REST contents API, so its commits are authored bygithub-actions[bot]and signed by GitHub;- PR Validation runs
Build/Update-DependencyLock.ps1 -Check, which fails while the lock file, the manifests and the SBOM disagree, or while a pinned hash no longer matches what the URL serves.
For a bump that passes the policy below, Dependabot validation starts PR Validation itself; any other
bump waits for a maintainer's /validate as before. A maintainer merges either way. Note that pushing
to a Dependabot branch also stops Dependabot rebasing it; @dependabot recreate gets a fresh one.
To do it by hand:
./Build/Update-DependencyLock.ps1 -Refresh # repin versions, hashes and SBOM from Build/Dependencies
./Build/Update-DependencyLock.ps1 -Check # verify without changing anythingSome pins are deliberately held back, each recorded with a PinReason in the lock file, and each with
a matching ignore rule in .github/dependabot.yml so Dependabot does not
keep proposing an update that cannot be taken:
Selenium.WebDriverfor Windows PowerShell 5.1 stays at 4.11.0, the last release that still ships anet4*build;System.Text.Jsonstays on the 8.x line, the last one that still targetsnet462;- the rest of the System.Text.Json closure moves only when System.Text.Json does. The module loads
those assemblies with
Assembly.LoadFrom, which applies no binding redirects, so the versions loaded have to be the ones the pinned System.Text.Json resolves — bumping one on its own is not a change this module can take.
-Check asserts that last rule against the Dependabot configuration itself, so a closure member
cannot be added to the lock without an ignore rule landing with it.
All of them still receive advisories — the frozen Selenium pin has its own manifest under
Build/Dependencies/Legacy for exactly that reason, and ignore suppresses version updates without
suppressing alerts — but an advisory against any of them needs a human decision rather than an
automatic bump.
-AuthenticationType WebView2 is the default, so every user hits the WebView2 assemblies. Fetching
them at runtime had two costs that pinning does not address:
- It fails outright without egress to nuget.org, which is the normal state of a locked-down corporate machine, and there was no supported way to pre-stage the files.
- Verification and loading were separated in time and place. The bytes were checked at download
time and then loaded much later, with
Assembly.LoadFrom, out of a user-writable directory that nothing re-checked. Anything running as that user could swap a DLL in between, and executing from a user-writable path is what WDAC and AppLocker policies commonly block.
So they are fetched and hash-verified during the build instead, by
Build/Get-BundledDependency.ps1, which reuses the module's own
Invoke-DownloadFile and the same pin in DependencyLock.psd1 — one verification implementation, not
two. The result ships in the package as
lib\<Core|Desktop>\<win-x64|win-x86>\, alongside ThirdPartyNotices.txt reproduced from the
package that was bundled, and loads from the module's install directory, which is usually read-only.
The binaries are not committed to this repository; the build downloads them on the runner.
The runtime download path is unchanged and still there. A module without a complete bundle — built
from source, trimmed, or imported with -UpdateDependencies — downloads and verifies exactly as
before. Nothing is ever copied out of the bundle into Bin, and the bundle itself is never written
to.
Everything else stays on the download path, deliberately:
msedgedriver.exehas to match the Microsoft Edge build on the user's machine, so it cannot be packaged at all. It stays on the download-and-Authenticode-verify path permanently.- Selenium and its dependency closure — 13 of the 14 locked artefacts — are retired by the
deprecation schedule in
OmadaWeb.PS/Private/Get-OmadaDeprecationSchedule.ps1after 2027-03-01, so bundling them now would be shipping payload that is about to be deleted.
The cost is package size: roughly 30 KB before, about 3.4 MB after, for every user including those
who only use -AuthenticationType Browser. The build prints the measured size so this stays visible.
.github/workflows/dependabot-validation.yml starts PR
Validation for a Dependabot pull request — instead of waiting for /validate — only when all of
these hold. Anything else gets a comment explaining which condition failed and waits for a maintainer.
- Update type. Every dependency in it is a semver patch or minor update. Dependabot bundles a dependency with the transitive updates it needs; one major among them makes the pull request wait as a whole.
- Release age. The new version was published at least 3 days ago — the registry's publication date
via deps.dev for a NuGet package, the GitHub release's
published_atfor an action. This keeps a security update, which Dependabot raises the moment an advisory is published, from being validated automatically inside the window in which a compromised release is typically caught and yanked. - Reputation. The source repository's OpenSSF Scorecard, as published by
deps.dev, scores at least 7.0 overall, is at most 45 days old, and scores 10 on
Dangerous-Workflow. No Scorecard, or no source repository deps.dev can link a package to, means no reputation and a maintainer's decision. - Provenance. Every commit is authored by
dependabot[bot]— or, on a NuGet bump, by the lock refresh asgithub-actions[bot]— with a signature GitHub verified, and the pull request modifies only the files its ecosystem may change. A commit pushed by anyone else makes it a normal pull request.
It never merges. A maintainer reviews the green result and merges, and that merge is what starts
release.yml — so no dependency version reaches the PowerShell Gallery without a person deciding it
should. The workflow runs on main's definition via pull_request_target and never checks out the
pull request: on an Actions bump, the pull request's own copy of the workflows already references the
new versions, which must not run with write permissions before they have been vetted.
What the Scorecard does and does not buy: it measures the project's security practices, not the release, and it can be gamed by a project optimising for the checks. Together with the age window, SHA pinning and the hash-verified lock file it raises the bar; it is not a guarantee.
The components described under Runtime dependency verification never pass through a package manifest — most are downloaded at runtime, and the bundled WebView2 assemblies are fetched by the build — so they need to be enumerated separately to be auditable.
To that end, every release has a CycloneDX SBOM (OmadaWeb.PS-<version>.cdx.json)
attached as a release asset. It covers the module itself, the bundled WebView2 assemblies and every
runtime-downloaded component, including source URL, license, how the component reaches the user
(omadaweb:acquisition, bundled or runtime-download) and — for components resolved during the
release build — the resolved version and SHA-256 hash of each file.
The inventory the SBOM is generated from is Build/Dependencies.psd1, and
it can be regenerated locally:
./Build/New-Sbom.ps1 -ModuleVersion '2026.8.13.1' -OutputPath ./OmadaWeb.PS.cdx.json