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
22 changes: 22 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5

- package-ecosystem: npm
directory: /sdks/typescript
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
11 changes: 7 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
python-tests:
name: Python Tests (${{ matrix.python-version }})
Expand All @@ -15,10 +18,10 @@ jobs:
python-version: ["3.11", "3.12"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -46,10 +49,10 @@ jobs:
node-version: ["18", "20"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node-version }}

Expand Down
49 changes: 30 additions & 19 deletions .github/workflows/issue-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
issues: write
steps:
- name: Validate spec request
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const body = context.payload.issue.body || '';
Expand All @@ -29,28 +29,39 @@ jobs:
return;
}

let parsed;
try {
const resp = await fetch(specUrl, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
if (resp.ok) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Spec URL is reachable (HTTP ${resp.status}). Thanks for including the direct link!`
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Spec URL returned HTTP ${resp.status}. Please double-check the link.`
});
}
} catch (e) {
parsed = new URL(specUrl);
} catch {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Could not reach the spec URL. Please verify the link is publicly accessible.'
body: 'The spec URL is not a valid absolute URL. Please provide a direct public HTTPS link.'
});
return;
}

if (
parsed.protocol !== 'https:' ||
parsed.username ||
parsed.password ||
!parsed.hostname
) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'The spec URL must be a direct public HTTPS link without embedded credentials.'
});
return;
}

// Do not fetch reporter-controlled URLs from a privileged Actions
// runner. Maintainers can inspect the URL during normal triage.
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'The spec URL has a valid HTTPS format. Thanks for including the direct link!'
});
7 changes: 5 additions & 2 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
release:
types: [published]

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
Expand All @@ -12,9 +15,9 @@ jobs:
run:
working-directory: sdks/typescript
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ jobs:
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"

Expand All @@ -24,4 +25,4 @@ jobs:
run: python -m build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to LAP (Lean API Platform) will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.1] - 2026-07-30

### Security
- **Registry-controlled command injection** -- launch login URLs with shell-free process APIs and validate server-provided authentication URLs before opening them
- **Registry URL validation** -- require HTTPS except for real loopback development hosts and reject credentials, malformed URLs, and prefix-bypass payloads
- **Issue automation SSRF** -- stop privileged GitHub runners from fetching reporter-controlled URLs

### Changed
- **Dependency hardening** -- update `js-yaml` and pin GitHub Actions to reviewed commit SHAs

## [0.7.0] - 2026-03-26

### Added
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

| Version | Supported |
|---------|-----------|
| 0.6.x | Yes |
| < 0.6 | No |
| 0.7.x | Yes |
| < 0.7 | No |

Only the latest release receives security fixes. We recommend always running the most recent version.

Expand Down
2 changes: 1 addition & 1 deletion lap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""LAP -- Lean API Platform. Token-efficient API specs for AI agents."""

__version__ = "0.7.0"
__version__ = "0.7.1"
52 changes: 50 additions & 2 deletions lap/cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"""

import json
import ipaddress
import os
import stat
import sys
import urllib.parse
import urllib.request
import urllib.error
import webbrowser
Expand All @@ -24,9 +26,55 @@
CREDENTIALS_FILE = CREDENTIALS_DIR / "credentials.json"


def _validate_web_url(value, *, label, allow_query=True):
"""Validate a web URL, allowing plaintext HTTP only for loopback hosts."""
if not isinstance(value, str) or not value or value != value.strip():
raise ValueError(f"{label} must be a valid absolute URL")
if "\\" in value or any(ord(char) < 0x20 or ord(char) == 0x7f for char in value):
raise ValueError(f"{label} must be a valid absolute URL")

try:
parsed = urllib.parse.urlsplit(value)
hostname = parsed.hostname
# Accessing port forces urllib to reject malformed/out-of-range ports.
parsed.port
except ValueError as exc:
raise ValueError(f"{label} must be a valid absolute URL") from exc

if not hostname:
raise ValueError(f"{label} must be a valid absolute URL")
if parsed.username is not None or parsed.password is not None:
raise ValueError(f"{label} must not contain credentials")
if not allow_query and (parsed.query or parsed.fragment):
raise ValueError(f"{label} must not contain a query string or fragment")

is_loopback = hostname.lower() == "localhost"
if not is_loopback:
try:
is_loopback = ipaddress.ip_address(hostname).is_loopback
except ValueError:
pass

if parsed.scheme == "https":
return value
if parsed.scheme == "http" and is_loopback:
return value
raise ValueError(f"{label} must use HTTPS (HTTP is allowed only for loopback development)")


def validate_registry_url(value):
"""Validate a registry base URL and normalize trailing slashes."""
return _validate_web_url(value, label="Registry URL", allow_query=False).rstrip("/")


def validate_auth_url(value):
"""Validate a browser authentication URL returned by the registry."""
return _validate_web_url(value, label="Authentication URL")


def get_registry_url():
"""Get registry URL from env or default."""
return os.environ.get("LAP_REGISTRY", DEFAULT_REGISTRY).rstrip("/")
"""Get and validate the registry URL from the environment or default."""
return validate_registry_url(os.environ.get("LAP_REGISTRY", DEFAULT_REGISTRY))


# ── Credentials ─────────────────────────────────────────────────────
Expand Down
18 changes: 8 additions & 10 deletions lap/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ def cmd_login(args):
"""Authenticate with the LAP registry via GitHub OAuth."""
from lap.cli.auth import (
api_request, save_credentials, load_credentials,
poll_sse_stream, get_registry_url,
poll_sse_stream, get_registry_url, validate_auth_url,
)
import webbrowser

Expand All @@ -409,7 +409,10 @@ def cmd_login(args):
result = api_request("POST", "/auth/cli/session", body=body if body else None)
session_id = result["session_id"]
stream_key = result["stream_key"]
auth_url = result["auth_url"]
try:
auth_url = validate_auth_url(result["auth_url"])
except (KeyError, ValueError) as exc:
error(f"Registry returned an invalid authentication URL: {exc}")

# Open browser
print(f"Opening browser for GitHub authorization...")
Expand Down Expand Up @@ -794,14 +797,9 @@ def _is_valid_skill_name(name: str) -> bool:

def _validate_registry_url(url: str) -> str:
"""Ensure registry URL uses HTTPS (except localhost for dev)."""
for prefix in ("http://localhost:", "http://localhost/", "http://127.0.0.1:", "http://127.0.0.1/"):
if url.startswith(prefix):
return url
if url in ("http://localhost", "http://127.0.0.1"):
return url
if not url.startswith("https://"):
raise ValueError(f"Registry URL must use HTTPS: {url}")
return url
from lap.cli.auth import validate_registry_url

return validate_registry_url(url)


def _register_session_hook(target: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "lapsh"
version = "0.7.0"
version = "0.7.1"
description = "Lean API Platform -- Token-efficient API specs for AI agents"
readme = "README.md"
license = "Apache-2.0"
Expand Down
22 changes: 16 additions & 6 deletions sdks/typescript/package-lock.json

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

4 changes: 2 additions & 2 deletions sdks/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lap-platform/lapsh",
"version": "0.7.0",
"version": "0.7.1",
"description": "TypeScript SDK for LAP (Lean API Platform) -- Parse and work with LAP API specifications",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down Expand Up @@ -51,7 +51,7 @@
"typescript": "^5.9.3"
},
"dependencies": {
"js-yaml": "^4.1.1"
"js-yaml": "^4.3.0"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.20.0"
Expand Down
Loading
Loading