Skip to content

Commit c1cb387

Browse files
committed
fix(security): address code review findings
- mkdocs-deploy.yml: move permissions from workflow level to job level (build job: contents:read; deploy job: pages:write, id-token:write) to follow least-privilege principle - examples/build_demo_db.py: add _validate_db_path() to reject path traversal (../) in CLI-provided database paths - scripts/_create_demo_db.py: add same path traversal validation - package.json: commit package-lock.json for reproducible ctxlint dependency resolution (remove from .gitignore)
1 parent 7e6c531 commit c1cb387

5 files changed

Lines changed: 82 additions & 23 deletions

File tree

.github/workflows/mkdocs-deploy.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,15 @@ on:
99
- '.github/workflows/mkdocs-deploy.yml'
1010
workflow_dispatch:
1111

12-
permissions:
13-
contents: read
14-
pages: write
15-
id-token: write
16-
1712
concurrency:
1813
group: pages
1914
cancel-in-progress: false
2015

2116
jobs:
2217
build:
2318
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
2421
steps:
2522
- uses: actions/checkout@v4
2623
- uses: actions/setup-python@v5
@@ -41,6 +38,9 @@ jobs:
4138
deploy:
4239
needs: build
4340
runs-on: ubuntu-latest
41+
permissions:
42+
pages: write
43+
id-token: write
4444
environment:
4545
name: github-pages
4646
url: ${{ steps.deployment.outputs.page_url }}

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ examples/notebooks/batch_config.yaml
7878

7979
# Node.js (for ctxlint)
8080
node_modules/
81-
package-lock.json
8281

8382
# mkdocs build output (deployed via GitHub Actions, not committed)
8483
site/

examples/build_demo_db.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,18 @@
136136
"""
137137

138138
# 演示数据库中预期的表
139-
_EXPECTED_TABLES = frozenset({
140-
"organizations", "members", "projects", "tasks",
141-
"reviews", "tags", "task_tags", "attachments",
142-
})
139+
_EXPECTED_TABLES = frozenset(
140+
{
141+
"organizations",
142+
"members",
143+
"projects",
144+
"tasks",
145+
"reviews",
146+
"tags",
147+
"task_tags",
148+
"attachments",
149+
}
150+
)
143151

144152
# ---------------------------------------------------------------------------
145153
# 公开 API
@@ -149,6 +157,22 @@
149157
_DEFAULT_DB_PATH = _SCRIPT_DIR / "sqlseed_demo.db"
150158

151159

160+
def _validate_db_path(path: Path) -> Path:
161+
"""Validate database path to prevent path traversal and unsafe locations.
162+
163+
Ensures the resolved path stays within the script's parent directory tree
164+
or is an absolute path explicitly provided by the caller (not relative ../).
165+
"""
166+
resolved = path.resolve()
167+
# Reject paths that escape to parent directories via relative ../ traversal
168+
# when the input was a relative path
169+
if not path.is_absolute():
170+
rel_input = str(path).replace("\\", "/")
171+
if rel_input.startswith("../") or "/../" in rel_input:
172+
raise ValueError(f"Path traversal not allowed: {path}")
173+
return resolved
174+
175+
152176
def ensure_db(db_path: str | Path | None = None) -> Path:
153177
"""确保演示数据库存在且 schema 正确。
154178
@@ -165,32 +189,27 @@ def ensure_db(db_path: str | Path | None = None) -> Path:
165189
Returns:
166190
数据库文件的绝对路径。
167191
"""
168-
path = Path(db_path) if db_path else _DEFAULT_DB_PATH
192+
path = _validate_db_path(Path(db_path) if db_path else _DEFAULT_DB_PATH)
169193

170194
if path.exists():
171195
conn = sqlite3.connect(str(path))
172-
existing = {
173-
r[0]
174-
for r in conn.execute(
175-
"SELECT name FROM sqlite_master WHERE type='table'"
176-
).fetchall()
177-
}
196+
existing = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
178197
if _EXPECTED_TABLES.issubset(existing):
179198
conn.close()
180-
return path.resolve()
199+
return path
181200
# Schema 不完整 — 修复
182201
conn.executescript(SCHEMA_SQL)
183202
conn.commit()
184203
conn.close()
185-
return path.resolve()
204+
return path
186205

187206
# 数据库不存在 — 仅创建 schema
188207
path.parent.mkdir(parents=True, exist_ok=True)
189208
conn = sqlite3.connect(str(path))
190209
conn.executescript(SCHEMA_SQL)
191210
conn.commit()
192211
conn.close()
193-
return path.resolve()
212+
return path
194213

195214

196215
def build(db_path: str | Path | None = None) -> Path:
@@ -204,7 +223,7 @@ def build(db_path: str | Path | None = None) -> Path:
204223
Returns:
205224
创建的数据库文件路径。
206225
"""
207-
path = Path(db_path) if db_path else _DEFAULT_DB_PATH
226+
path = _validate_db_path(Path(db_path) if db_path else _DEFAULT_DB_PATH)
208227
if path.exists():
209228
path.unlink()
210229

@@ -213,7 +232,7 @@ def build(db_path: str | Path | None = None) -> Path:
213232
conn.executescript(SCHEMA_SQL)
214233
conn.commit()
215234
conn.close()
216-
return path.resolve()
235+
return path
217236

218237

219238
if __name__ == "__main__":

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/_create_demo_db.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,21 @@
77
from pathlib import Path
88

99

10+
def _validate_db_path(path: Path) -> Path:
11+
"""Validate database path to prevent path traversal attacks.
12+
13+
Rejects relative paths that escape to parent directories via ../.
14+
"""
15+
if not path.is_absolute():
16+
rel_input = str(path).replace("\\", "/")
17+
if rel_input.startswith("../") or "/../" in rel_input:
18+
raise ValueError(f"Path traversal not allowed: {path}")
19+
return path.resolve()
20+
21+
1022
def main() -> None:
1123
db_path_str = sys.argv[1] if len(sys.argv) > 1 else "quickstart_demo.db"
12-
db_path = Path(db_path_str)
24+
db_path = _validate_db_path(Path(db_path_str))
1325
if db_path.exists():
1426
db_path.unlink()
1527

0 commit comments

Comments
 (0)