From 21716568a5497f50289a722e3a9d3cec78cedcbd Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 3 Sep 2026 00:19:56 +0000 Subject: [PATCH] package: reject unsafe project names --- README.md | 2 +- docs/CURRENT_STATUS.md | 1 + docs/ROADMAP.md | 2 +- docs/V1_16_PACKAGES_PROJECTS.md | 8 +++++++- src/claro.c | 3 ++- tools/validate_package_security.py | 21 ++++++++++++++++++++- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index df60d8b..07d930c 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ packages/ Starter projects created with `claro new MyProject` use the same `manifest-version: 1` and `lock-version: 1` headers as `claro package init`, so the first project files match the package maintenance tools. -Package names are checked so unsafe names such as `../bad` are rejected when adding packages. Names must also be 64 characters or fewer, which keeps generated package paths predictable. If an unsafe name is already present in `claro.project`, `claro package doctor`, `claro package lock`, and lockfile refreshes during `claro package remove` flag it instead of treating it as safe lockfile data. +Project names and package names are checked so unsafe names such as `../bad` are rejected before Claro creates folders. Names must also be 64 characters or fewer, which keeps generated project and package paths predictable. If an unsafe package name is already present in `claro.project`, `claro package doctor`, `claro package lock`, and lockfile refreshes during `claro package remove` flag it instead of treating it as safe lockfile data. ## Standard-library path and collection helpers diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index de0e704..ac9d2d3 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -97,6 +97,7 @@ Ready now: - `claro package add/list/remove/doctor/lock` - local project files such as `claro.project`, `claro.lock`, and `packages/` - starter projects from `claro new` now use the same manifest-version and lock-version headers as `claro package init` +- project-name safety checks for `claro new`, so unsafe names such as `../bad` are rejected before Claro creates folders - package-name safety checks when adding packages, including the 64-character package-name limit, when `claro package doctor` audits an existing `claro.project`, when `claro package lock` writes lockfile data, and when `claro package remove` refreshes the lockfile after an edit Still needed: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 127f8ce..82cebaf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -31,7 +31,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix. 1. Keep beginner-facing docs current and separate from historical release notes. 2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately. -3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, and CI workflow coverage validation). Current package safety validation covers safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, and making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object `CHECK TYPE` and direct field assignment diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, field collection after simple methods, NUMBER/TEXT/YESNO-valued unknown-field diagnostics, and beginner-facing fallback wording when an unknown-field assignment expression type is not inferable yet. +3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, and CI workflow coverage validation). Current package safety validation covers safe project creation, rejecting unsafe project names during `claro new`, safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, and making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object `CHECK TYPE` and direct field assignment diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, field collection after simple methods, NUMBER/TEXT/YESNO-valued unknown-field diagnostics, and beginner-facing fallback wording when an unknown-field assignment expression type is not inferable yet. 4. Add small examples for each foundation feature before adding bigger syntax. ## Complete-platform milestones diff --git a/docs/V1_16_PACKAGES_PROJECTS.md b/docs/V1_16_PACKAGES_PROJECTS.md index 1a05014..a31ac57 100644 --- a/docs/V1_16_PACKAGES_PROJECTS.md +++ b/docs/V1_16_PACKAGES_PROJECTS.md @@ -65,7 +65,13 @@ checksum: 1234abcd `claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. -## Safety rules +## Project-name safety + +`claro new NAME` checks the project name before creating folders. Project names may use only letters, numbers, dash, and underscore, and they must be 64 characters or fewer. + +This keeps mistakes like `../bad` from creating a project outside the folder where the learner is working. + +## Package-name safety Package names may use only: diff --git a/src/claro.c b/src/claro.c index 784643e..9b1d08d 100644 --- a/src/claro.c +++ b/src/claro.c @@ -618,7 +618,8 @@ static int run_tests(void){ int fails=0; printf("%s: %d failure(s)\n",fails?"FAIL":"PASS",fails); return fails?1:0; } static int write_text_file_simple(const char *path,const char *text){ FILE *f=fopen(path,"wb"); if(!f) return 0; fputs(text,f); fclose(f); return 1; } -static int create_new_project(const char *name){ char path[512]; char text[1024]; if(!name||!*name){ fprintf(stderr,"Project needs a name. Try: claro new MyProject\n"); return 1; } if(!make_folder(name)){ fprintf(stderr,"Could not create project folder: %s\n",name); return 1; } snprintf(path,sizeof(path),"%s/main.claro",name); write_text_file_simple(path,"SAY \"Welcome to Claro!\"\n\nSET name TO \"Learner\"\nSAY \"Hello \" + name\n\nTEACH greet person\n SAY \"Nice to meet you, \" + person\nEND\n\nDO greet \"Friend\"\n"); snprintf(path,sizeof(path),"%s/packages",name); make_folder(path); snprintf(path,sizeof(path),"%s/claro.project",name); snprintf(text,sizeof(text),"manifest-version: 1\nname: %s\nmain: main.claro\nversion: v1.18.26\npackages:\n",name); write_text_file_simple(path,text); snprintf(path,sizeof(path),"%s/claro.lock",name); write_text_file_simple(path,"# Claro package lock\nlock-version: 1\nversion: v1.18.26\n"); snprintf(path,sizeof(path),"%s/README.md",name); write_text_file_simple(path,"# My Claro Project\n\nBuild Claro, then run this project with:\n\n```bash\nclaro run\n```\n\nUse packages with:\n\n```bash\nclaro package add text\nclaro package list\nclaro package doctor\n```\n"); printf("Created Claro project: %s\n",name); printf("Next steps:\n cd %s\n claro run\n",name); return 0; } +static int project_name_safe(const char *name){ int i; if(!name||!*name) return 0; if(strlen(name)>64) return 0; if(name[0]=='.'||name[0]=='-'||name[0]=='_') return 0; for(i=0;name[i];i++){ unsigned char c=(unsigned char)name[i]; if(!(isalnum(c)||c=='_'||c=='-')) return 0; } return 1; } +static int create_new_project(const char *name){ char path[512]; char text[1024]; if(!name||!*name){ fprintf(stderr,"Project needs a name. Try: claro new MyProject\n"); return 1; } if(strlen(name)>64){ fprintf(stderr,"Project names must be 64 characters or fewer.\n"); return 1; } if(!project_name_safe(name)){ fprintf(stderr,"Project names may use only letters, numbers, dash, and underscore.\n"); return 1; } if(!make_folder(name)){ fprintf(stderr,"Could not create project folder: %s\n",name); return 1; } snprintf(path,sizeof(path),"%s/main.claro",name); write_text_file_simple(path,"SAY \"Welcome to Claro!\"\n\nSET name TO \"Learner\"\nSAY \"Hello \" + name\n\nTEACH greet person\n SAY \"Nice to meet you, \" + person\nEND\n\nDO greet \"Friend\"\n"); snprintf(path,sizeof(path),"%s/packages",name); make_folder(path); snprintf(path,sizeof(path),"%s/claro.project",name); snprintf(text,sizeof(text),"manifest-version: 1\nname: %s\nmain: main.claro\nversion: v1.18.26\npackages:\n",name); write_text_file_simple(path,text); snprintf(path,sizeof(path),"%s/claro.lock",name); write_text_file_simple(path,"# Claro package lock\nlock-version: 1\nversion: v1.18.26\n"); snprintf(path,sizeof(path),"%s/README.md",name); write_text_file_simple(path,"# My Claro Project\n\nBuild Claro, then run this project with:\n\n```bash\nclaro run\n```\n\nUse packages with:\n\n```bash\nclaro package add text\nclaro package list\nclaro package doctor\n```\n"); printf("Created Claro project: %s\n",name); printf("Next steps:\n cd %s\n claro run\n",name); return 0; } static int run_repl(void){ Runtime rt; char line[4096]; Program p; rt_init(&rt); memset(&p,0,sizeof(p)); p.path=xstrdup(""); p.count=1; p.cap=1; p.lines=(char**)xmalloc(sizeof(char*)); printf("%s\n",CLARO_VERSION); printf("Type HELP for help, or EXIT to leave.\n"); while(1){ int pc=0; printf("> "); fflush(stdout); if(!fgets(line,sizeof(line),stdin)) break; { size_t n=strlen(line); while(n&&(line[n-1]=='\n'||line[n-1]=='\r')) line[--n]=0; } { char *t=trim_inplace(line); if(ci_eq(t,"EXIT")||ci_eq(t,"QUIT")) break; if(ci_eq(t,"HELP")){ printf("Try: SAY \"Hello\"\n"); printf("Try: SET name TO \"Jon\"\n"); printf("Try: SAY name\n"); continue; } if(!*t) continue; p.lines[0]=t; exec_line(&rt,&p,&pc,t); if(rt.error){ fprintf(stderr,"%s:%d: %s\n",rt.err_file?rt.err_file:"",rt.err_line,rt.err_msg?rt.err_msg:"error"); rt_clear_error(&rt); } } } return 0; } static int file_exists_simple(const char *path); diff --git a/tools/validate_package_security.py b/tools/validate_package_security.py index 45425a6..f4a4da5 100644 --- a/tools/validate_package_security.py +++ b/tools/validate_package_security.py @@ -31,7 +31,26 @@ def main(): fail(f"Expected {EXPECTED_VERSION}, got:\n{out}") with tempfile.TemporaryDirectory() as td: - work = Path(td) + root = Path(td) + work = root / "work" + work.mkdir() + + rc, out = run([str(EXE), "new", "../bad"], work) + if rc == 0: + fail("Unsafe project names with path separators must be rejected") + if "Project names may use only" not in out: + fail(f"Unsafe project-name diagnostic was unclear:\n{out}") + if (root / "bad").exists(): + fail("Unsafe project name should not create a folder outside the current project area") + + long_project_name = "P" * 65 + rc, out = run([str(EXE), "new", long_project_name], work) + if rc == 0: + fail("Project names longer than 64 characters must be rejected before paths are created") + if "Project names must be 64 characters or fewer" not in out: + fail(f"Long project-name diagnostic was unclear:\n{out}") + if (work / long_project_name).exists(): + fail("Long project name should not create a project folder") rc, out = run([str(EXE), "new", "StarterApp"], work) if rc != 0: