T09 · Insecure Skill Coding Practices
- Location
scripts/graph_sync.py:307- Finding
Graph-controlled section paths permit filesystem escape, arbitrary writes, moves, and recursive deletion
- Content
View full analysis
Vulnerability Details
File Location:
scripts/graph_sync.py:307-312, 551-554, 732-734;scripts/html2md.py:580-602
Vulnerability Type: Path traversal through remotely supplied notebook section hierarchy
Risk Level: HighTechnical Analysis
OneNote section and section-group display names returned by Microsoft Graph are used to construct the
relpath without rejecting absolute paths,..components, or path separators:python def walk_group(g, group_id, prefix): """recursively enumerate section groups → [(relpath, section_id, name, lastModifiedDateTime)]""" log(f" ↳ group {prefix}") out = [] d, s, _ = g.json(f"{API}/me/onenote/sectionGroups/{group_id}/sections?$select=id,displayName,lastModifiedDateTime") if s == 200: for sec in d.get("value", []): out.append((os.path.join(prefix, sec["displayName"]), sec["id"], sec["displayName"], sec.get("lastModifiedDateTime", ""))) d, s, _ = g.json(f"{API}/me/onenote/sectionGroups/{group_id}/sectionGroups?$select=id,displayName") if s == 200: for grp in d.get("value", []): out.extend(walk_group(g, grp["id"], os.path.join(prefix, grp["displayName"]))) return outAlthough cache paths are sanitized elsewhere, the corresponding store path uses the raw
relvalue. Section rename handling can therefore move directories outside the intended store root:python old_s = os.path.join(STORE_DIR, old_rel) new_s = os.path.join(STORE_DIR, rel) if os.path.isdir(old_s) and not os.path.isdir(new_s): os.rename(old_s, new_s)Disappeared-section cleanup uses the same unvalidated value in a recursive deletion operation:
python store_p = os.path.join(STORE_DIR, rel) if os.path.isdir(store_p): shutil.rmtree(store_p)The HTML-to-Markdown conversion path also writes and cleans files beneath directories constructed from the raw section path:
python dest_dir = os.path.join(STORE_DIR, rel) os.mak ...[truncated 3333 chars]- Remediation
View remediation
Remediation Suggestions
-
Map each remote hierarchy component independently
- Do not preserve remote display names as filesystem paths.
- Convert every section and group name into a single safe local segment.
- Reject or encode
/,\, NUL/control characters,.,.., drive prefixes, and absolute paths. - Prefer stable section/group IDs for local directory names and retain display names only as metadata.
-
Enforce canonical containment before every filesystem mutation
- Resolve the intended notebook store root with
os.path.realpath(). - Resolve each candidate path before creating, writing, moving, removing, or recursively deleting it.
- Require the candidate to remain beneath the canonical root, preferably with
os.path.commonpath(). - Reject the operation if containment cannot be proven.
Example pattern:
python def confined_path(root, *parts): root_real = os.path.realpath(root) candidate = os.path.realpath(os.path.join(root_real, *parts)) if os.path.commonpath([root_real, candidate]) != root_real: raise RuntimeError(f"path escapes store root: {candidate}") return candidate - Resolve the intended notebook store root with
-
Apply the guard to every affected operation
save_htmland Markdown destination construction.- Section rename source and destination paths.
- Orphan-file removal.
- Disappeared-section
shutil.rmtree()cleanup. - Any future report or cache path derived from remote names.
-
Harden destructive cleanup
- Refuse recursive deletion of the store root itself or any ancestor.
- Consider deleting only files recorded as generated artifacts rather than recursively deleting a remotely named directory.
- Use stable IDs in the manifest to identify managed directories.
-
Add regression tests
- Test names including
..,.,../../target, absolute POSIX paths, Windows drive and UNC paths, mixed separators, and Unicode separator-like characters. - Veri ...[truncated 130 chars]
- Test names including
-
