Skip to content
View ntdat812's full-sized avatar
🥰
Welcome to my profile
🥰
Welcome to my profile
  • Light Road Co., Ltd.
  • Thanh Hoa, Vietnam
  • 21:10 (UTC +07:00)

Block or report ntdat812

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
ntdat812/README.md

Nguyen Thanh Dat — AI gateways, agent tooling, security review

Merged Advisories Open Projects

English · Tiếng Việt

Software engineer in Thanh Hoa, Vietnam. I work on AI gateways and agent tooling — the layer between a coding agent and three hundred model providers, where one correctness bug becomes every user's bug. I read the issue nobody has picked up, reproduce it, and follow it to the line that is actually wrong.

Every patch below ships with a regression test that I verify fails on the base branch before I open the pull request. Where a repository has no test runner, it carries a written reproduction instead.

The record

Counted 25 August 2026, from gh pr list -R <repo> --author ntdat812, one repository at a time. "Merged" means the change is in the upstream default branch of a repository I do not own. Nothing in my own repositories is counted. Every number here links to the list behind it.

Count What it counts
Pull requests merged 22 Landed in the default branch of a repo I don't own
Closing someone else's issue 11 Of those 22, the ones that close a filed issue
Security advisories 5 Reported privately; all five now fixed
Pull requests open 38 Opened, awaiting review
Repositories 7 Third-party repos I've contributed to

I hold no push, merge or admin right on any of these projects. Everything below was reviewed and merged by somebody who does.


Merged

diegosouzapw/OmniRoute — MIT AI gateway, one endpoint in front of 350 providers, 54.5k★. Twenty merged. volcengine/OpenViking — context database for agents, 33.0k★. One merged. lidge-jun/opencodex — universal provider proxy, 12.0k★. One merged.

Eleven of the twenty-two close an issue somebody else filed. Full list. Twelve that show the range:

Pull request What it fixes
#10843 fix(security) An SSRF guard that matched cloud-metadata hosts by spelling instead of by address. Detailed below.
#11328 fix(security) The canonical denylist of headers never forwarded upstream was missing two of the RFC 7230 hop-by-hop names, so proxy-authorization and proxy-authenticate went to the provider.
opencodex #2476 fix(responses) A 24 MiB state file was re-serialised and atomically replaced every two seconds whether or not anything in it had changed — and nothing reads it until the next start. The snapshot is now compared before it is written, by length and digest rather than by keeping the payload, and the debounce scales with the size.
#11380 test(kimi) A nightly run reported one failure in 8,280 and it was being read as a Node 26 compatibility break. The test drew a random number and then asserted on the outcome. I fixed the test and said the issue should stay open, because a flaky test is not the thing it was filed about.
#11376 fix(auth) Every upstream failure that was not already a string collapsed to the literal Provider error, and that is the line an operator reads. A refused port, a DNS failure, a blocked proxy and a provider simply saying no were indistinguishable — the actionable part sits on error.cause.code, which nothing looked at.
#11319 fix(db) The proxy-URL validator refused private and cloud-metadata targets using its own dotted-quad regexes, so the same address in another spelling walked straight through.
#10935 fix(relay) An earlier fix put the attacker-controlled x-relay-path behind a guard in the Deno and Vercel relay workers. The Cloudflare worker still concatenated it, so userinfo in the path re-pointed the request past the private-host check.
#10868 fix(proxy) Every egress probe had moved to an IPv6-first endpoint, so an IPv4-only tunnel had no route to it, hung until the deadline, and a proxy carrying live traffic was reported dead. The strategy was wrong, not the constant. Closes #9694.
#10862 fix(providers) Model sync did not fail on an upstream 401. It quietly degraded to a cached catalog, so a provider with dead credentials still looked healthy. Closes #9683.
#10858 fix(context) Base64 documents were measured character by character, so a 1 MB PDF estimated at 350,022 tokens and the request was rejected before it ever left. Closes #10840.
#10853 fix(i18n) Eight locales rendered the status "Disabled" as the noun for a person who has a disability. Closes #10812.
OpenViking #4228 fix(ov_dream) A session message whose content was a plain string rather than a block list was not accepted. Closes #4221.

One of them, in detail

isCloudMetadataHost() guards the classic SSRF pivot: an attacker who can steer an outbound request at 169.254.169.254 reads the instance's IAM credentials. The code documented this block as unconditional. It was not.

The guard compared the hostname against a set of dotted-decimal strings. But WHATWG URL serialises an IPv4-mapped IPv6 literal as hextets, so the same address arrives wearing a different spelling:

http://[::ffff:169.254.169.254]/
        │
        └─ new URL(…).hostname  ─►  "::ffff:a9fe:a9fe"
                                     │
                                     ├─ in CLOUD_METADATA_HOSTNAMES?  no
                                     └─ startsWith("169.254.")?        no
                                                                       │
                                            routes to 169.254.169.254 ─┴─►  allowed

The fix folds the embedded IPv4 back out before deciding — a9fe and a9fe are two hextets that have to be decoded to 169.254.169.254, not string-matched — so the verdict follows the address rather than its spelling. Reading the surrounding function turned up a second gap: :: is the IPv6 twin of 0.0.0.0 and reaches a service on the IPv6 loopback, but only the IPv4 spelling was refused. That is in the same patch.

Evidence I reported with it: the new test fails 10 of 12 cases on release/v3.8.50 and passes 12 of 12 with the change; the guard's five existing suites stay green at 73 of 73.

And the same shape keeps coming back. A check that compares how something is spelled against a list, when what decides the outcome is what it is. Since that patch I have found it in a proxy-URL validator (#11319), a group pattern compiled into a RegExp without escaping (#11311), a --no-verify block that missed git's abbreviated long options (ECC #2837), the same block again with --config-env as a second spelling of -c (ECC #2858), a destructive-command classifier hidden by a sudo prefix (ECC #2832), and a dev-server block reading raw text where it should read tokens (ECC #2846). The fix is the same sentence every time: decide on identity, not on spelling.

Once I found it pointing the other way, which was the more useful lesson. A dedupe key compared a usage row's millisecond and its fields, and two genuinely different requests compared equal, so one was thrown away as a duplicate (9router #3544). The same confusion — spelling stood in for identity — but it merged two things instead of letting one through. Whichever way it points, the question to ask the code is the same one.


Security research

Five vulnerabilities reported through OmniRoute's private security advisories, the disclosure route the project's SECURITY.md asks for. All five are fixed.

Two by patches I sent: the cloud-metadata bypass described above, and the relay-path gap that #10935 closed in the Cloudflare worker.

The other three were fixed upstream, with the advisory named in the code that does it — GHSA-mghq-58h3-qcqj and GHSA-v7g9-7f55-5g46 on the always-protected route list in src/server/authz/routeGuard.ts, GHSA-wgwc-crjm-pmwv on the loopback-only entry beside it.

That first pair is the one I would point at. GHSA-mghq was the report. GHSA-v7g9 is what came out of going back and reading the fix instead of trusting it — two sibling routes had been left behind, still reachable the same way as before. Re-reading a patch that has already been accepted is not interesting work, and it is the step most people skip.

None of the five was published as an advisory, so the patched code is the only public record of them. Grep the repository for the ids.


In review

Thirty-eight open: twenty-one on 9router (26.2k★), six on ECC (243k★), five each on ComfyUI (130k★) and OpenViking (33.0k★), one on OpenClaw (387k★). Nothing of mine is left open on OmniRoute or opencodex. Full list.

Pull request What it fixes
OpenClaw #127135 Every request to an Alibaba Model Studio provider sent the output-token cap as max_completion_tokens — a field the vendor's own OpenAI-compatibility reference does not list. Closes #127119.
9router #3538 fix(transport) A model pinned to a different wire format got its body translated but not its destination: 9router serialised a Claude request and posted it to the provider's OpenAI endpoint, with that endpoint's auth. Upstreams parse what they recognise and drop the rest, so it half-worked. Closes #3418 and #3439.
9router #3544 fix(usage) The dedupe query keyed a usage row on its millisecond timestamp plus the request's fields. Two genuine requests in the same millisecond compare equal, so one of them is discarded as a duplicate — 100 parallel writes recorded 2. The suite had been failing on master; it was read as a transaction race, and the driver is synchronous.
9router #3522 fix(tunnel) The public subdomain a tunnel is published under was drawn from a predictable source, so the address meant to be unguessable could be guessed.
9router #3517 fix(proxy) A loopback request was sent out through the configured outbound proxy, so a request to the machine itself left the machine. Closes #3424.
ComfyUI #15841 A YAML list in extra_model_paths.yaml crashed the loader instead of being read as a list of paths.
ComfyUI #15783 A model directory that links back to one of its own ancestors makes the walk re-enter the same tree at every level, so one model is listed over and over. Following links is deliberate; detecting the loop was missing.
OpenViking #4233 The memory plugin's URI guard read file content as if it were a path. Closes #4188.
OpenViking #4229 A stale PID lock was honoured on macOS without checking that the process holding it was the one it claimed to be. Closes #4210.
ECC #2858 The guard that stops a commit from skipping its hooks knew -c core.hooksPath=. git --config-env=core.hooksPath=VAR is the same instruction read from the environment, and it was not on the list — so the hook did not run and the commit went through. Verified against git 2.51.
ECC #2846 The dev-server block decided the script name from raw text rather than from tokens.
ECC #2837 The guard blocking --no-verify compared the flag against that exact spelling. Git resolves any unambiguous prefix of a long option, so --no-ver skipped the hooks.

How I work

Four things I would rather be judged on than a language list. Each links to the artefact.

I report what the tests actually said. I could not get a green build on my Windows box for #10843, so instead of ticking the box I showed the identical failure on a clean checkout of the base branch, traced it to an optional native dependency, and said plainly that a real CI run would be more authoritative than mine.

I widen a report when the bug is wider. #10812 reported one bad Japanese string. The same mistranslation was in eight locales and 24 strings, so #10853 fixed all of them and added glossary entries so the next translator does not repeat it.

I build what was agreed, not what I would rather build. opencodex #2476 had an obvious large answer — replace the whole snapshot store with a journal. Triage had endorsed two narrow measures instead, so I implemented those two and wrote in the pull request that the journal direction was deliberately not attempted. It merged.

I stand down when someone was there first. I opened 9router#3434 eleven minutes after #3433 fixed the same defect, having missed it while checking for duplicates. I closed mine, said why, and left the two regression cases my branch covered on theirs.


What I work on

Node, TypeScript, and Python. Streaming HTTP and server-sent events. Compatibility between the OpenAI, Claude, and Gemini request shapes — where they agree on paper and diverge in practice. Access control, SSRF review, and command-classification guards. Internationalisation, which turns out to be a correctness problem more often than a translation one.


Contact

ntdat812.dev@gmail.com

Pinned Loading

  1. openclaw/openclaw openclaw/openclaw Public

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    TypeScript 388k 81.4k

  2. affaan-m/ECC affaan-m/ECC Public

    The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.

    JavaScript 243k 36.8k

  3. Comfy-Org/ComfyUI Comfy-Org/ComfyUI Public

    The most powerful and modular diffusion model GUI, api and backend with a graph/nodes interface.

    Python 130k 15.3k

  4. diegosouzapw/OmniRoute diegosouzapw/OmniRoute Public

    Never stop coding. Free MIT AI gateway: one endpoint, 350 providers (90+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Clin…

    TypeScript 54.8k 7.5k

  5. volcengine/OpenViking volcengine/OpenViking Public

    Self-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.

    Python 33.2k 2.5k

  6. decolua/9router decolua/9router Public

    Unlimited FREE AI coding. Connect Claude Code, Codex, Cursor, Cline, Copilot, Antigravity to FREE Claude/GPT/Gemini via 40+ providers. Auto-fallback, RTK -40% tokens, never hit limits.

    JavaScript 26.3k 4.8k