Skip to content

Fix the P2 findings of the security and performance audit - #129

Merged
0xfauzi merged 25 commits into
mainfrom
ccr-01549f8f-6750vr
Oct 26, 2026
Merged

0xfauzi merged 25 commits into
mainfrom
ccr-01549f8f-6750vr

Conversation

@0xfauzi

@0xfauzi 0xfauzi commented Oct 26, 2026

Copy link
Copy Markdown
Owner

This change fixes the P2 findings of the audit. #128 had the P0 and P1 fixes.

Security

  • server-1, server-2: systemap live and systemap serve refuse a POST request if Origin is a different origin or null, or if Sec-Fetch-Site is cross-site. Before this change, a web page in the browser could start a full render. A request without these headers (curl, Node fetch) still works. The servers also have a timeout for the request read, a limit on the /events readers, and a backoff after an accept error.
  • ci-2, ci-5: the PR summary puts each name from the repository in a code span and escapes @. Thus repository text cannot make a link, an image or a mention. The comment output of the workflow and of the init workflow template uses a random heredoc delimiter.
  • ci-1: the plugin examples use an installed systemap or uvx systemap, not uv run systemap. uv run systemap builds the project of the repository. The trust text is correct now.
  • ci-3: a CI job runs pre-commit run --all-files, with gitleaks, zizmor, actionlint and shellcheck.
  • llm-1, g1ts-7, llm-4: model text, agent text, decision text, commit subjects and file names go through one function before terminal output. That function shows control characters and line separators as visible escapes. A sequence id that the agent gives must be a slug.

Robustness

  • exec-3, g1ts-4, g1full-3: deep nesting in one module gives that module an unknown record with the new cause depth. Before, it stopped the full extraction. The TypeScript walks use a stack, not recursion.
  • g1ts-8, g1full-6: judgement, check, render and facts examine the shape of a stored map.json. A malformed file gives an error that tells you to run systemap refresh, not a traceback. refresh can repair the file.
  • llm-2: the page compaction divides lines only at LF. U+2028, U+2029 and U+0085 are escaped in each JSON value in a script. Before, one such character in model text stopped the page script.
  • llm-3: decisions submit refuses text that is not valid UTF-8 before it writes, so it writes nothing.
  • llm-5, regex-3: each pattern in answer_migrations.py has its fixed text at its fixed position, and each variable part has a limit. Each pattern has a negative test.

Performance

Finding Before After
TypeScript call sites, 20 files of 1.8 KB (g1full-3) 120.7 s 1.0 s
systemap check on this repository (scale-2) 13.2-13.4 s 6.1-6.2 s
Python namespace set (rest-3) one set for each module one set for each extraction
long [judgement] answered line (regex-3) 84 s at 132 KB bounded

The performance changes keep the same output. Each branch compared the map, map.json and the figures of this repository before and after the change.

Open items

These items stay open:

  • delta and history read stored facts without the shape check.
  • A package.json that is nested 5000 deep can still give RecursionError in typescript_config.package_json.
  • jev_cli.cmd_audit calls decision_kinds.warnings without the current lines.
  • A manual check is necessary to find whether the hook host $.http.fetch (hooks/register.tsx) sends Origin or Sec-Fetch-Site. If it sends them, the refresh of the agent hook gets 403. Node fetch sends neither, and the server accepts it.

Checks

These checks pass on the head:

  • uv run pytest: 2311 passed, 7 skipped
  • uv run pre-commit run --all-files
  • uv run mypy src --strict
  • uv run systemap refresh
  • uv run systemap check
  • uv run systemap judgement: no open findings, 16 current reviews

The official ASD-STE100 dictionary was not available. A maintainer must examine the words of the new text.


Generated by Claude Code

claude added 25 commits October 26, 2026 08:49
…s (scale-2)

One command renders the same map for the page, each figure, the check and the
stale check. Each render calculated route_all, place_labels, assurance.of_model
and evidence.of_model again. route.routed() now keeps the routes and the label
positions by the repr of all inputs. assurance.of_model and evidence.of_model
keep their results by a digest of the pickle of all inputs. Each cache is
bounded and gives a copy or frozen records, so a caller cannot change it.

On this repository (3 + 5 runs each, frozen copy at 136975f):
check 13.14 s -> 6.45 s, refresh (current) 13.18 s -> 6.27 s,
refresh (stale) 32.40 s -> 7.60 s. Router.route calls: 400 -> 80 (check),
1120 -> 80 (stale refresh). The written outputs are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
…pping extraction

Findings g1full-3, exec-3 and g1ts-4 of the audit.

The TypeScript call-site reader makes one walk from the root with a stack. The walk
records the parent, the innermost named function and the binding flag of each node.
Each property chain gets its dotted name one time. The scope reader keeps the
declaring scope of each name on each path. The TypeScript walks, the syntax tokens,
the test names, the binding patterns and the package.json exports targets use a
stack, not recursion.

If a parse or a walk of one module gives RecursionError or MemoryError, the module
stays in the facts with an unknown record of the new cause `depth`. A test file gives
a test_file_issues record. The Python tree cache refuses such a text, so later steps
do not read its tree.

Measured: 20 TypeScript files of 1.8 KB take 1.0 s in systemap extract (before:
120.7 s). The deep fixtures of the audit exit 0. The outputs of this repository are
byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
rest-3: python_unresolved calculated the namespace package set again for each
module, so the extraction time grew with the square of the module count. The
Python context now keeps one set for each extraction (NamespaceCache), and
namespace_packages uses str.rfind prefixes. The facts output does not change.

g1ts-8 and g1full-6: read_facts now examines the shape of the stored facts
(facts_shape): the types of the fields that extraction writes, the keys that the
readers index, relative file paths, and a nesting limit of 32 levels. A RecursionError
from the JSON parser is also a shape error. An incorrect file gives FactsError (a
ConfigError) with exit code 1 and the action "Use systemap refresh or systemap
extract to write a new facts file". extract reads the stored facts only for
--check, and refresh reports an incorrect file as stale, so the two commands can
write a new file in place of an incorrect file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
ci-2: The PR summary builds each gap detail from a fixed form and the
repository names. Each name is a code span. A detail without names gets
a word joiner after each "@", so it starts no mention.

ci-5: The delta step of this repository and of the init workflow writes
the comment output with a random heredoc delimiter. A comment line that
is equal to the old delimiter does not end the output.

ci-1: plugin.json, README.md and the coding agents page do not give
"uv run systemap" as a plugin command. They tell the reader not to use
it, because it builds the repository project. The trust sentence says
that it is not true if the command runs repository code.

ci-3: A new pre-commit CI job runs all hooks on all files, and the
manual gitleaks-tree hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
…rver

server-1: The page servers now refuse a POST request with 403 when
Sec-Fetch-Site is present and is not same-origin or none, or when Origin
is present and is not the served origin. A program without these headers,
for example curl or the agent hook, can still send POST /refresh.

server-2: Each connection has a 10 s read time limit. The live server
accepts at most 32 event connections and refuses more with 503. When
accept fails because the process has no free descriptor, the server
stops for 0.1 s before the next accept.

The new module systemap.serve_guard is part of the CLI component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
llm-2: _compact split the page script with str.splitlines. A U+2028,
U+2029 or U+0085 in map text cut the DETAIL line, and the page script
stopped. _compact now splits only at a line feed. The new line_safe
function escapes U+2028, U+2029 and U+0085 in the detail JSON and in
the workspace JSON of the page.

llm-7: journeys.add_to_source mixed str.splitlines offsets with the
rows of the tokenizer, and with_imports mixed them with the rows of
ast. Both now split the lines as the Python reader does.

llm-3: decisions submit now refuses a result with text that UTF-8
cannot encode (for example a lone surrogate) before it writes a file.
answer_write changes UnicodeEncodeError into AnswerWriteError before
the write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
Add terminal_text.printable. It replaces C0 and C1 control characters,
DEL, U+2028, U+2029 and unpaired surrogates with a visible \uXXXX escape.
Other text does not change.

- cli.say, cli.warn, failures.report, the serve printer and the jev_cli
  output use terminal_text.lines, which keeps the newline.
- Finding lines in judgement and delta reports, commit subjects in
  history, sequence ids, labels and agent problems in journeys, and
  request ids and file names in decisions errors use printable.
- history divides git output only at the newline. Before, U+2028 in a
  commit subject gave two commit lines.
- journeys refuses an id that is not a slug, an empty id, and a label or
  step sentence with a control character.
- decisions submit refuses a summary with a control character.

Finding identifiers do not change. The output of check, judgement,
history, decisions and refresh for this repository is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
…ex-3)

Each pattern in answer_migrations now has its fixed text at its fixed
position, and each variable part has a limit. A name or a reason in a
current line can no longer move the fixed text, and a match is linear
in the length of the line.

An item that is a current line stays the same: judgement and audit give
the current lines to current_line as `present`. An item longer than
LONGEST_MIGRATED (4096 characters) stays the same. A cache makes the
pattern work one time for each item.

Measured: a 132 KB item took 7.5 s, 15.2 s and 83.4 s in current_line,
and apply_answers with 200 lines took more than 600 s. Now each takes
less than 0.01 s. The judgement output of this repository is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
… review)

The cache keys of assurance.of_model and evidence.of_model are the digest of
the pickle of all inputs. A Meaning with a valid Mapping field that pickle
cannot copy, such as types.MappingProxyType or a Mapping class in a trusted
model file, stopped check, refresh and render with a TypeError. The base code
accepted this input.

The new function evidence.remembered() makes the key. If pickle cannot copy
the inputs, it calculates the result and does not keep it. Both of_model
functions use it. A new test gives a MappingProxyType and an unpicklable
Mapping class, and compares the claims, the flow evidence and the SVG with
the result for a dict. Both cases failed before this change.

The module docstring of tests/test_render_cache.py now agrees with the test
(three renders, one call each) and with the recorded 32.4 s.

On the frozen copy at 136975f (3 runs each): check 6.00 s, refresh (current)
6.68 s, refresh (stale) 7.82 s. Router.route calls: 80. The output hashes
are identical to the runs before and after the first cache change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
Give one error text for JSON that has too many levels. Split the
namespace_packages docstring into short sentences. Change the test comment
so that it states what the refresh test guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
…ript file

Review findings of the g1full-3, exec-3 and g1ts-4 change.

The search for the error node in parse_problem uses a stack, not recursion. A deep
TypeScript file with a syntax error now gives the cause `syntax` and the error line,
not the cause `depth` with line 0. The surface reader uses pattern_names of the scope
reader for the names of an exported binding pattern. Thus a deep exported pattern
keeps its surface.

The `depth` row of the reference and the DEPTH fix line now say that some facts of the
file are not known. If a later step fails, the surface stays.

Measured with `systemap extract` on a fixture with two files 3000 deep: before, the two
modules had cause `depth` and line 0. After, the file with the error has cause `syntax`
and line 1, and the pattern file is parsed with the name `a`. The facts of five normal
fixtures are byte-identical before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
The coding agents page is built again from its source. The committed page
agrees with the build, and the site test passes.

The trust sentence in plugin.json, README.md and the coding agents page
now gives a literal statement: if the command option runs code of the
repository, the code of the repository can run when the live map starts.

The map is refreshed. The flows CLI -> Scaffold, Config -> Scaffold and
Scaffold -> Model cite the current digest of systemap.scaffold. The
source reviews of Scaffold, ChangeDetector, Check, CLI, Config and Model
were examined again and record the current digests. The claims did not
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
schematic_script.py had a raw U+2028 and a raw U+2029 in _LINE_ENDS. Two
test files had them also. The fact code counts lines with str.splitlines,
so the loc of schematic_script was 477 and not 475, and each cited row
after line 14 was two rows off. The files now use escapes, and a test
makes sure that no source file or test file has such a character.

The AnswerWriteError for text that UTF-8 cannot encode now says only that
the command did not write the target file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
Refresh the map for the new module systemap.serve_guard. The CLI
description now says that the page servers refuse a POST request from a
page of a different web site. Record the current CLI source review digest.

The trust and security guide page no longer says that the servers have no
other access control. It gives the cross-site refusal, the read time
limit and the maximum number of event connections. Rebuild docs/guide.

The accept test closes the real socket before it puts the test socket in
its place, so the test does not leave a socket open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
If the answer has no id, the sequence id is the entry point name, as before.
Names such as 'GET /a' and 'pkg.reader:read' are not refused. An entry point
name with a control character is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
The judgement command gives the current lines to answer_migrations.warnings.
An item that is a current line gets no "Change it to" warning. Before this
change, 4 exact current lines got a warning with a different line.

The entry kind identifier pattern accepts a component name with a space.
The name must be the same in the two positions.

The map output is refreshed, so systemap check passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
# Conflicts:
#	docs/map/index.html
#	docs/map/map.json
# Conflicts:
#	docs/map/index.html
#	docs/map/map.json
#	map/model.py
# Conflicts:
#	src/systemap/failures.py
#	src/systemap/serve.py
# Conflicts:
#	docs/map/figures/control.svg
#	docs/map/figures/system.svg
#	docs/map/index.html
#	docs/map/map.json
Merge results of fix/p2-calls, fix/p2-render, fix/p2-facts, fix/p2-ci,
fix/p2-server, fix/p2-textterm, fix/p2-textenc and fix/p2-migrations:

- Refresh docs/map for the merged source.
- Correct the source records of 51 flows whose cited modules changed
  (assurance, audit, check, cli, delta_report, evidence, extract,
  facts_file, jev_cli, journeys, judgement, page, route, schematic), and
  record their review digests after an examination of each flow claim.
- Record the source reviews of 16 components after an examination of
  their claims. No claim text became false.
- Add one CHANGELOG block for the P2 fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3Co8X9K3cT1u4rYQ9VwU
@github-actions

Copy link
Copy Markdown

Architecture

  • Contract: no change
  • Map: 3 changed components
    • changed component CLI (description, modules): examined, source review current
    • changed component Config (modules): examined, source review current
    • changed component FactsExtractor (modules): examined, source review current
  • Rules: none

Levels: established = the facts show the claim. examined = an agent or a person examined the cited source. unknown = the facts and the source reviews do not show the claim.

@0xfauzi
0xfauzi merged commit 65e5464 into main Oct 26, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants