T09 · Insecure Skill Coding Practices
- Location
scripts/add_candidate.py:9- Finding
Candidate PII and Hiring Assessments Stored Without Enforced Restrictive Permissions
- Content
View full analysis
Vulnerability Details
File Location:
scripts/add_candidate.py:9-27, 34-52;scripts/update_pipeline.py:8-20, 64-69
Vulnerability Type: Plaintext sensitive-data storage with ambient filesystem permissions
Risk Level: MediumVulnerable Code
scripts/add_candidate.py:9-27, 34-52python RECRUITING_DIR = os.path.expanduser("~/.openclaw/workspace/memory/recruiting") PIPELINE_FILE = os.path.join(RECRUITING_DIR, "pipeline.json") def ensure_dir(): os.makedirs(RECRUITING_DIR, exist_ok=True) def load_pipeline(): if os.path.exists(PIPELINE_FILE): with open(PIPELINE_FILE, 'r') as f: return json.load(f) return {"candidates": []} def save_pipeline(data): ensure_dir() with open(PIPELINE_FILE, 'w') as f: json.dump(data, f, indent=2) parser.add_argument('--job-id', required=True, help='Job ID') parser.add_argument('--name', required=True, help='Candidate name') parser.add_argument('--email', required=True, help='Candidate email') parser.add_argument('--source', default='Direct', help='Application source') args = parser.parse_args() candidate_id = f"CAND-{str(uuid.uuid4())[:6].upper()}" candidate = { "id": candidate_id, "job_id": args.job_id, "name": args.name, "email": args.email, "source": args.source, "applied_at": datetime.now().isoformat(), "current_stage": "applied", "stage_history": [ {"stage": "applied", "entered_at": datetime.now().isoformat(), "exited_at": None} ], "rating": "pending", "notes": "" }scripts/update_pipeline.py:8-20, 64-69python RECRUITING_DIR = os.path.expanduser("~/.openclaw/workspace/memory/recruiting") PIPELINE_FILE = os.path.join(RECRUITING_DIR, "pipeline.json") def load_pipeline(): if not os.path.exists(PIPELINE_FILE): return {"candidates": []} with open(PIPELINE_FILE, 'r') as f: retu ...[truncated 2631 chars]- Remediation
View remediation
Remediation Suggestions
-
Create the recruiting directory with owner-only permissions and verify the mode even when it already exists:
python os.makedirs(RECRUITING_DIR, mode=0o700, exist_ok=True) os.chmod(RECRUITING_DIR, 0o700) -
Write records through a securely created temporary file with mode
0600, flush and synchronize it, and atomically replace the destination:python import tempfile def save_pipeline(data): os.makedirs(RECRUITING_DIR, mode=0o700, exist_ok=True) os.chmod(RECRUITING_DIR, 0o700) fd, temp_path = tempfile.mkstemp(dir=RECRUITING_DIR) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as f: json.dump(data, f, indent=2) f.flush() os.fsync(f.fileno()) os.replace(temp_path, PIPELINE_FILE) os.chmod(PIPELINE_FILE, 0o600) except Exception: try: os.unlink(temp_path) except FileNotFoundError: pass raise -
Apply equivalent protections to every file containing recruiting data, including job, candidate, interview, communication, and pipeline records.
-
Add a migration or startup check that identifies and repairs overly permissive modes on existing directories and files. Where ACLs are supported, verify that they do not grant unintended access.
-
Minimize stored personal information, define retention and deletion procedures, and avoid placing sensitive personal data in free-form notes.
-
If organizational policy or the threat model requires protection from privileged local readers, use authenticated encryption at rest with keys stored separately from the recruiting files.
-
Add automated tests that create files under a permissive test umask and assert that final directory and file modes remain
0700and0600, respectively.
-
