Skip to content

Platform v2.11.0: close obfuscation and SMPP bypasses, honest benchmarks (model 2.7 unchanged) - #229

Merged
ajamous merged 17 commits into
mainfrom
data-cleanup-step3
Sep 18, 2026
Merged

ajamous merged 17 commits into
mainfrom
data-cleanup-step3

Conversation

@ajamous

@ajamous ajamous commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Platform v2.11.0, shipping as release candidate v2.11.0-rc.1. The classifier stays model 2.7, so there is no classification regression risk. What changes is everything around it: several ways to bypass the model, a proxy policy that delivered unsure verdicts, benchmarks that did not describe production, and a Docker image that did not contain the shipped weights.

Why

Attackers could get past OpenTextShield without beating the model at all.

Bypass Before After
Fullwidth letters (PayPal) 42.6% blocked 100%
Look-alike letters outside the old map (ӏ ѕ ԁ) 80.9% blocked 100%
All obfuscation styles (n=544) 92.1% 99.3%

Alongside that, the SMPP proxy forwarded every spam or phishing verdict below confidence_threshold as ham. Across the saved benchmark predictions that delivered 564 IMC25 phishing messages while preventing only 5 false blocks.

What is in here

  • Text cleanup is context-aware. The old folding rewrote real Russian and Greek into mixed-script text and mapped Cyrillic у to u through a duplicate key. The new one changes zero predictions on ~19,000 benchmark messages, where the old one flipped 18.
  • Unsure verdicts are acted on. New below_threshold_action defaults to use_label; forward_as_ham restores the old behaviour.
  • Sender-controlled skips closed: shift headers on readable text, plain-ASCII ISO-2022-JP, messages over 512 characters, and text hidden in message_payload while short_message looks harmless.
  • Startup config validation. An unknown rule action used to leave the client with no submit_sm_resp.
  • Docker images ship only what they should. Docker ignore patterns do not cross /, so *.pth only matched root-level files: model 2.7 was never actually excluded, and an earlier claim in this branch that main's images lack the model was wrong (a real build disproved it). The same gap did ship an unreleased 711 MB checkpoint, audit_logs/ (full SMS text) and infra/ (1.3 GB of Terraform providers). Now excluded; context 2.8 GB → 1.4 GB.
  • The loader refuses a Git LFS pointer. That is the real way an image starts and cannot classify: a checkout without git lfs pull leaves a ~130 byte pointer that passes the existence check and fails later inside torch.load.
  • Evaluation matches production. run_eval.py, calibrate_thresholds.py and finetune_tier1.py now apply the same text cleanup as the API (--raw-text reproduces older numbers).
  • Honest eval sets: a fable5 split with training twins removed (71 rows) and 40 legitimate alerts that look like scams, for measuring false blocks.
  • Training data cleaned, with a written labeling rule (docs/LABELING_GUIDE.md, evals/label_audit.py). Tooling and evidence only, see below.

What is deliberately not in here

Model 2.8. Retraining on the cleaned data was measured across three seeds per configuration. It catches roughly 290 more attacks per 8,000 real-world messages but adds about 35 false blocks per 10,000 legitimate ones, and is slightly worse on obfuscated text. That is a trade to choose deliberately, not an upgrade, so model 2.7 stays. Full evidence, including the seed spread that invalidated earlier single-run comparisons, is in evals/results/DATA_CLEANUP_2.8.md.

Testing

Suite Result
API unit tests 39/39
SMPP offline (npm test) 137/137
SMPP integration against model 2.7 47/47 and 32/32, identical to the unchanged proxy

The SMPP message helpers moved into message_utils.js so the tests exercise the real code instead of hand-copied versions that could drift.

Upgrade notes

  • Behaviour change: unsure spam and phishing verdicts are now blocked. Set classification.below_threshold_action to forward_as_ham if false blocks are the bigger risk for your traffic.
  • Startup is strict: a typo in a rule action now stops the proxy with a clear error instead of hanging clients.
  • No model file changes, so no Git LFS pull is needed to upgrade.

Validating before merge

No images are published for a release candidate. Build locally from this branch; Docker Hub tags follow after validation and merge. Steps are in evals/results/RELEASE_NOTES_v2.11.0.md.

On live traffic the number to watch is the false-block rate on A2P alerts.

Validated locally (2026-09-18)

Run on an Apple Silicon machine against the branch tip with model 2.7. Full table in evals/results/RELEASE_NOTES_v2.11.0.md.

Claim Result
Unsure verdicts acted on 10 below-threshold spam/phishing verdicts kept their label and were rejected
Long message no longer skipped 661 chars via message_payload: classified phishing 0.9999, rejected
Hidden second field no longer skipped harmless short_message + phishing message_payload: rejected
Fullwidth obfuscation phishing 1.000; real Russian and Greek unharmed
Startup validation bad rule action and bad below_threshold_action both exit 1 with a clear error
Suites integration 47/47 and 32/32, API 42/42, SMPP offline 137/137
Benchmarks (model 2.7) fable5 clean 55/55 blocked, 1/16 false blocks; hard legit 13/40; Mishra 98.8% block, 0.5% false blocks
Docker image builds, serves model 2.7, blocks the fullwidth lure

… intact

The homoglyph map folded every Cyrillic/Greek look-alike in every message,
turning real Russian and Greek into mixed-script text, and a duplicate key
mapped Cyrillic "у" to "u" ("uour"). It also missed fullwidth and math-bold
letters and several common look-alikes.

- Fold compatibility letters and digits (fullwidth, math-bold) to ASCII
- Fold fullwidth punctuation only in mostly-Latin messages (disguised links)
- Fold Cyrillic/Greek look-alikes only in mixed-script words, or in
  all-look-alike words inside a mostly-Latin message
- Extend the look-alike table (palochka, dze, je, shha, komi de, Greek caps)

Local check with model 2.7: zero prediction changes on ~19k benchmark
messages (the old folding flipped 18), and obfuscated phishing blocked
99.3% (fullwidth 52% -> 100%, unmapped look-alikes 93% -> 100%).
run_eval.py and calibrate_thresholds.py tokenised raw text, while the API
normalises every message first, so benchmark numbers did not describe the
deployed pipeline for non-ASCII traffic. Both now apply the same cleanup via
loaders.production_normalize. Pass --raw-text to reproduce older results.
- Below confidence_threshold the proxy forwarded every spam/phishing verdict
  as ham. New below_threshold_action defaults to use_label; forward_as_ham
  keeps the old behaviour. On the benchmarks the downgrade delivered 564
  extra IMC25 phishing messages while avoiding 5 false blocks.
- Classify single shift headers on text without escapes, and ISO-2022-JP
  payloads that are plain ASCII, instead of skipping them. Shift headers on
  non-GSM data coding are flagged. New unclassifiable_action: forward|reject.
- Classify short_message and message_payload together when both are set.
- Fit text over 512 characters to the API limit (head and tail) instead of
  failing the call and forwarding unclassified.
- Validate classification config at startup and reject malformed API replies.
- Move pure message helpers to message_utils.js so tests exercise the real
  code instead of vendored copies. Add test_message_utils.js and npm test.
- Sync package-lock.json with the iconv-lite dependency in package.json.

Tests: npm test 137/137; test_smpp.js 47/47 and test_advanced.js 32/32
against model 2.7, identical to the unchanged proxy.
One approved rule for ham, spam and phishing across training data, test
sets and label reviews, aligned with how the fable5 suite is labeled.
Freeze the exact rows model 2.7 trained on (synthetic set plus the seed-7,
2,500-per-label rehearsal sample) and relabel them under
docs/LABELING_GUIDE.md.

- Approved decisions applied by pattern, matched on a folded form so
  augmented copies (prefixes, emoji, look-alike letters, leetspeak) are
  covered: prize and "WIN X NOW, click to claim" messages to phishing, IRS
  money demands to phishing, generic job ads to spam, WhatsApp codes to ham,
  bare links, junk codes and translator/assistant notes removed.
- TypeSafe (jev-1.13.0) labeled every unique text as a reviewer only. Its
  answers are stored in typesafe_answers.jsonl so builds need no API key.
- Flag groups were accepted or rejected by review: prize/IRS upgrades and
  MobileNet ads accepted; real Netflix/Shopee/Voicespin notices kept as ham.
- 102 unclear flagged rows stay out of training until reviewed.

Result: 9,394 -> 9,029 rows; 1,498 labels changed, 263 rows removed.
Label mix: ham 2,998, spam 2,032, phishing 3,999.
…g set

Cleaning moved about 1,000 prize scams out of spam, so spam fell to 2,032
rows, and the old sample had few real service notices, which model 2.7
blocks too often. Add both from the rest of the v2.4 corpus.

- `candidates` samples 1,600 spam and 900 notice rows, excluding the
  training subset, every eval set (fable5, Mishra, OTS60, and local UCI and
  IMC25 dumps), augmented copies, rows the approved rules would change, and
  more than 40 notices per sender family.
- A candidate is kept only when TypeSafe agrees with its corpus label at
  confidence 0.9 or higher. Labels are never changed in this step.
- Accepted: 457 advertising spam, 548 legitimate notices (mostly one-time
  codes). A random read of 30 from each pool found no wrong labels.

train_v2.8_candidate.csv: 10,034 rows (ham 3,546, spam 2,489, phishing 3,999).
- fable5_adversarial_v1_clean.csv (71 rows) drops the 56 suite rows that
  share a template with synthetic_fable5_v1.csv, the synthetic set every
  model since 2.7 trains on. The token Jaccard check missed obfuscated and
  translated twins; two rows even had digit-for-digit copies in training.
  fable5_template_twins.json records each removed row, its closest training
  row and the same-template probability.
- hard_legit_a2p_v1.csv (40 rows): handwritten legitimate bank, delivery,
  billing, security and code messages that look like scams, to measure false
  blocks. Model 2.7 blocks 13 of them at argmax.
- datasets/README.md documents every eval set and how to run them.
- --train-csv trains on a prepared text,label file (every row, no rehearsal
  sampling), so the curated set from evals/label_audit.py can be used
  directly. --synthetic/--original still work as before.
- Texts get the same cleanup as the API and run_eval.py before tokenising;
  --raw-text reproduces older checkpoints.
- The trainlog now records lr, seed, raw_text and the training data used.
…e 2.8 runs

Adding legitimate notices on their own taught the model to pass anything shaped
like a bank or parcel message: IMC25 block rate fell 2.8 points and the newly
passed messages were real attacks (SBI PAN-card, Santander, BPOST, MijnOverheid).

- `candidates` now samples a third pool of notice-shaped phishing and caps each
  sender family per label, so notices are added in pairs. 329 accepted.
- train_v2.8_candidate.csv: 10,363 rows (ham 3,546, spam 2,489, phishing 4,328).
- evals/results/DATA_CLEANUP_2.8.md records four training mixes measured on the
  same benchmarks and recommends 2.8e: the only candidate that beats 2.7 on
  block rate without raising false blocks on real ham.

Model 2.7 stays in production. The candidate checkpoint is not committed.
…ed variance

- All 102 flagged rows reviewed (44 ham, 23 spam, 6 phishing, 29 removed as
  unclear or empty). Nothing is left undecided.
- evals/generate_notice_pairs.py builds notice-shaped lures with a benign twin
  for the same brand and topic, in 11 languages, dropping anything close to an
  eval message. TypeSafe accepted 385 lures and 451 twins.
- The pairs are opt-in (`build --with-notice-pairs`): they cut false blocks on
  legitimate-looking messages but cost obfuscation robustness.
- Three seeds per configuration show run-to-run spread reaching 6 points, which
  is larger than the differences earlier single runs seemed to show. Without the
  pairs the model over-blocks plain notices ("Your Amazon package was
  delivered", "PayPal: You sent $45.00").

Conclusion in evals/results/DATA_CLEANUP_2.8.md: no candidate is strictly better
than 2.7, so model 2.7 stays in production and the platform fixes ship alone.
…del 2.7)

Bump api_version, package __version__ and the OpenAPI spec to 2.11.0. The
package version and spec were stale at 2.9.0 while settings reported 2.10.0.

The shipped classifier stays model 2.7: retraining on the cleaned data trades
false blocks for block rate rather than beating 2.7 outright, so the data work
ships as tooling and evidence only. Release notes in
evals/results/RELEASE_NOTES_v2.11.0.md.
.dockerignore excluded every *.pth and re-included only model 2.5, but the API
has defaulted to 2.7 since v2.10.0. An image built from main starts and then
cannot classify: the loader only logs a warning when the weights are missing.

Both ignore files now list 2.7 alongside 2.5, which stays for the documented
OTS_MBERT_MODEL_PATH rollback. Release notes cover the local-build QA path; no
images are published for a release candidate.
Local validation of the RC disproved the reasoning in df7e5c0. Docker's ignore
patterns do not cross '/', so `*.pth` only ever matched root-level files: model
2.7 was never excluded and images built from main were never missing it. A real
docker build proved this.

The same gap did ship things it should not. Now excluded:
- mbert_ots_model_2.8-candidate.pth, an unreleased 711MB checkpoint
- audit_logs/ and feedback/, which hold full SMS text
- infra/, 1.3GB of Terraform state and provider binaries
- nested node_modules/ and __pycache__/

Patterns cross directories now, with explicit re-includes for the shipped 2.7 and
the 2.5 rollback target. Build context drops 2.8GB -> 1.4GB; the built image
contains exactly 2.5 and 2.7, no audit logs, no infra.

The real way an image starts and cannot classify is a checkout without
`git lfs pull`: the pointer file passes exists() and fails later inside
torch.load. model_loader now detects the pointer signature, logs the fix and
skips the model instead of pretending to serve it. Three tests cover it.

Release notes record the correction and the full local validation results.
Runs a candidate image for days under realistic, labeled SMPP traffic so an RC
can be judged on stability and behaviour over time, not just on test suites.

- build_corpus.py: 60k labeled messages from the repo's corpora (82% ham split
  between personal and A2P, 10% spam, 8% phishing), multilingual, with a slice
  obfuscated and a slice sent as long message_payload.
- soak_client.js: SMPP client with a diurnal rate and hourly bursts, records
  every outcome and latency, acks DLRs, reconnects if the proxy drops.
- monitor.sh: per-minute health, /metrics, container memory and restarts, audit
  and proxy log growth, proxy RSS.
- chaos.sh: restarts the dummy upstream every 6 hours to exercise reconnect.
- soak_report.py: outcomes per class, false blocks by pool, obfuscation,
  latency per hour, resource trend, proxy log counters.
- run_soak.sh / stop_soak.sh: detached launch and clean stop.

First run: ots:2.11.0-rc.2 for 48 hours, started 2026-09-18 20:19 UTC.
@ajamous
ajamous merged commit 196f6c6 into main Sep 18, 2026
4 checks passed
@ajamous
ajamous deleted the data-cleanup-step3 branch September 18, 2026 21:57
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.

1 participant