Stop the generator drafting a permit call spelled out in a string - #73
Merged
VSN2015 merged 1 commit intoSep 27, 2026
Merged
Conversation
executable_source only stripped comment tokens, so PERMIT_CALL/EXPECT_CALL
still scanned the literal CONTENT of string and heredoc tokens too. A
controller logging `params.require(:admin).permit(:superuser)` for humans
had :superuser drafted as a permitted field under :admin, a call that
never runs — the same class of wrong, security-flavoured suggestion the
comment-stripping exists to prevent.
Add masked_source: executable_source with string/heredoc/regexp CONTENT
replaced by a placeholder no keyword can spell out of, so PERMIT_CALL and
EXPECT_CALL are matched against it instead. Parens are left in place so a
lenient lex that swallows real code into an unterminated string keeps
closing an already-open call the same way it always did. A real call's
own string argument (`permit("name")`) still matches — `params`, `.`,
`permit`, and the surrounding parens are code tokens, never string
content — and scan reads each matched group's actual text back out of
executable_source by offset, since masked_source's own text may hold
placeholders.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VSN2015
pushed a commit
that referenced
this pull request
Sep 27, 2026
Bumps version.rb, retitles the Unreleased CHANGELOG section, and includes the regenerated Gemfile.lock — CI runs bundler in frozen mode, so a version bump without the lockfile fails the tag build. Twenty PRs since 0.8.0 (#16, #35, #51-#57, #59-#66, #71-#73): Rails 8 params.expect support and model-aware drafting in permittable:generate, the permittable:audit coverage command, reusable field groups (Permittable.fields/use), accept_params/reject_params RSpec matchers, RFC 9457 problem+json, named format: presets, and a type-checking mode for the schema-drift guard — plus a from-scratch Ruby-to-ECMA-262 pattern translator, a correctness overhaul of in: list casting and authored default:/example: storage, several encoding-crash and log-forging fixes, and three freshly-found gaps: an array field's required: + default: silently behaved as optional, accept_params/reject_params disagreed with a standalone Contract's own unknown: strictness, and the generator could draft a field from a permit call that only existed inside a log string. Minor rather than patch: mostly new surface, but a :string field's in: list of Symbols now matches correctly where it used to reject every request, and an array field combining required: and default: now fails at class load instead of silently treating the field as optional. See CHANGELOG.md for the complete list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
executable_sourcestrips comments before the generator scans a controller forparams.permit/params.expectcalls — the module's stated goal is to never draft a field from a line that doesn't run, since that would be "a wrong suggestion, and a security-flavoured one, from a line that does not run." But it only stripped comment tokens; string and heredoc content was deliberately kept, becausepermit("name")is a supported spelling and its keys live in string tokens.The trouble is
PERMIT_CALL/EXPECT_CALLthen scanned the whole reconstructed source with a plain regex — including the contents of ordinary strings that have nothing to do with a real call:bin/rails permittable:generatedrafted:superuseras a permitted field under an:adminroot from that log line — a call that never executes, reached through a string literal instead of a#comment.spec/generator_spec.rbhad a whole "not code" describe block for comments, trailing comments, and=begin/=end, but nothing exercising a string or heredoc whose content spells out a fake call.The fix
Added
masked_source:executable_sourcewith every string/heredoc/regexp literal's CONTENT masked out (every character replaced except(/), same length asexecutable_sourceso offsets still line up).PERMIT_CALL/EXPECT_CALLnow scanmasked_sourceinstead —params,permit,require, andexpectcan't spell out of a placeholder, so a call spelled out as text inside a string can no longer match. Parens are left in the mask so a lenient lex that swallows real code into an unterminated string (the existing "mismatched quotes" spec) still closes an already-open call the same way it always did.A real call's own string argument (
permit("name")) is unaffected:params,.,permit, and the surrounding parens are code tokens, never string content, so masking never touches them.scanreads each matched group's actual text back out ofexecutable_sourceby position rather than off the match itself, sincemasked_source's text may hold placeholders where a real argument's string content is.Verification
:admin/:superuserwere drafted from the log line above), then passing after the fix.bundle exec rspec: 849 examples, 0 failures.bundle exec rubocop lib/permittable/generator.rb spec/generator_spec.rb: no offenses.permit("name", 'email', :age), mismatched-quote fallback,#inside a string) still passes unchanged.🤖 Generated with Claude Code