Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

296 changes: 148 additions & 148 deletions requirements-test.txt

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Werkzeug==3.1.8
Flask-SocketIO==5.6.1
python-socketio==5.16.3
paramiko==5.0.0
# Compatibility: keep the reviewed Fernet, PBKDF2, and key-loading APIs within cryptography 49.
cryptography>=49.0,<50
# Compatibility: cryptography 50 fixes CVE-2026-69247 while retaining the reviewed Fernet, PBKDF2, and key-loading APIs.
cryptography>=50.0,<51
python-dotenv==1.2.2
Flask-Login==0.6.3
Flask-SQLAlchemy==3.1.1
Expand Down
296 changes: 148 additions & 148 deletions requirements.txt

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions scripts/lock_requirements.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[CmdletBinding()]
param(
[switch]$Check
[switch]$Check,
[switch]$Upgrade
)

Set-StrictMode -Version Latest
Expand All @@ -10,14 +11,23 @@ $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$temporaryDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ("webssh-lock-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null

if ($Check -and $Upgrade) {
throw "-Check and -Upgrade cannot be used together."
}

function New-LockFile {
param(
[Parameter(Mandatory)] [string]$InputFile,
[Parameter(Mandatory)] [string]$LockName
)

$temporaryLock = Join-Path $temporaryDirectory $LockName
& uv pip compile $InputFile --universal --generate-hashes --no-header --output-file $temporaryLock | Out-Null
$committedLock = Join-Path $projectRoot $LockName
if (Test-Path -LiteralPath $committedLock) {
Copy-Item -LiteralPath $committedLock -Destination $temporaryLock
}
$upgradeArguments = if ($Upgrade) { @("--upgrade") } else { @() }
& uv pip compile $InputFile --universal --python-version 3.11 --generate-hashes --no-header --output-file $temporaryLock @upgradeArguments | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "uv could not compile $InputFile."
}
Expand Down
18 changes: 16 additions & 2 deletions scripts/vendor.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,22 @@ const files = [
['material-icons/iconfont/material-icons.woff', 'material-icons/material-icons.woff'],
];

function contentsMatch(srcPath, destPath, dest) {
function expectedContents(srcPath, dest) {
const source = fs.readFileSync(srcPath);
if (dest !== 'socketio/socket.io.min.js') return source;

const vulnerableDecoder = 'if(o!=Number(o)||"-"!==t.charAt(i))throw new Error("Illegal attachments");r.attachments=Number(o)';
const patchedDecoder = 'if(o!=Number(o)||"-"!==t.charAt(i))throw new Error("Illegal attachments");var a=Number(o);if(!Number.isInteger(a)||a<1)throw new Error("Illegal attachments");if(a>10)throw new Error("too many attachments");r.attachments=a';
const bundle = source.toString('utf8');
const matches = bundle.split(vulnerableDecoder).length - 1;
if (matches !== 1) {
throw new Error('Socket.IO bundle decoder changed; review the CVE-2026-69185 patch.');
}
return Buffer.from(bundle.replace(vulnerableDecoder, patchedDecoder), 'utf8');
}

function contentsMatch(srcPath, destPath, dest) {
const source = expectedContents(srcPath, dest);
const destination = fs.readFileSync(destPath);
if (!/\.(?:css|js)$/.test(dest)) return source.equals(destination);
const normalize = value => value.toString('utf8').replace(/\r\n/g, '\n');
Expand All @@ -56,7 +70,7 @@ for (const [src, dest] of files) {
}
} else {
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(srcPath, destPath);
fs.writeFileSync(destPath, expectedContents(srcPath, dest));
console.log(` ${src} -> static/vendor/${dest}`);
}
count++;
Expand Down
2 changes: 1 addition & 1 deletion static/vendor/socketio/socket.io.min.js

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions tests/js/vendor-socketio-security.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const assert = require('node:assert/strict');
const test = require('node:test');

const io = require('../../static/vendor/socketio/socket.io.min.js');


function decoder() {
return new io.Manager('http://127.0.0.1', { autoConnect: false }).decoder;
}


test('vendored Socket.IO rejects binary packets with zero attachments', () => {
assert.throws(
() => decoder().add('50-["event"]'),
/Illegal attachments/,
);
});


test('vendored Socket.IO bounds the number of pending binary attachments', () => {
assert.throws(
() => decoder().add('511-["event"]'),
/too many attachments/,
);
});
23 changes: 23 additions & 0 deletions tests/test_dependency_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,29 @@ def test_lock_generator_compiles_universal_locks():
script = Path("scripts/lock_requirements.ps1").read_text(encoding="utf-8")
assert "uv pip compile" in script
assert "--universal" in script
assert f"--python-version {MINIMUM_PYTHON}" in script


def test_lock_generator_reuses_committed_pins_unless_upgrade_is_explicit():
script = Path("scripts/lock_requirements.ps1").read_text(encoding="utf-8")

assert "[switch]$Upgrade" in script
assert "Copy-Item -LiteralPath $committedLock -Destination $temporaryLock" in script
assert 'if ($Upgrade) { @("--upgrade") } else { @() }' in script


@pytest.mark.parametrize("path", ["requirements.txt", "requirements-test.txt"])
def test_runtime_locks_exclude_vulnerable_cryptography_versions(path):
"""CVE-2026-69247 affects cryptography 44 through 49."""
assert locked_version(path, "cryptography") >= Version("50.0.0")


def test_frontend_lock_excludes_vulnerable_socket_io_parser_versions():
"""CVE-2026-69185 affects socket.io-parser 4.0.0 through 4.2.6."""
package_lock = json.loads(Path("package-lock.json").read_text(encoding="utf-8"))
parser = package_lock["packages"]["node_modules/socket.io-parser"]

assert Version(parser["version"]) >= Version("4.2.7")


def test_python_runtime_contract_stays_synchronized():
Expand Down
Loading