Skip to content

Fix security attrs parse#2199

Open
exageraldo wants to merge 4 commits into
swaggo:masterfrom
exageraldo:fix-tags-parse-after-security-attrs
Open

Fix security attrs parse#2199
exageraldo wants to merge 4 commits into
swaggo:masterfrom
exageraldo:fix-tags-parse-after-security-attrs

Conversation

@exageraldo

Copy link
Copy Markdown

Hey folks.

Recently, I was working on API documentation and noticed that the information from the tags I was defining (@tag.name and @tag.description) wasn't being included in the documentation generated by swag CLI. When I searched for issues with the same or a similar problem, I found #1497 and then began to investigate the issue further.

While trying to reproduce the issue in tests, I realized that the problem wasn't with the flags passed to the CLI (such as --parseDependency or --dir), but rather with the order in which the information was presented in the function's documentation. In this case, when the security attributes come before the tag attributes.

@tag.name / @tag.description annotations are silently dropped from the generated Swagger docs when a @securityDefinitions.* block appears earlier in the same general API info comment group. No error or warning is produced — swagger.Tags simply comes back empty.

To Reproduce
Steps to reproduce the behavior:

  1. Create a Go file whose general API info comment block declares a security definition before any tags:
    package main
    
    // @title Some API Title
    // @version 0.0.1
    // @BasePath /
    // @schemes http https
    //
    // @securityDefinitions.apikey  Bearer
    // @in                          header
    // @name                        Authorization
    // @description                 Type "Bearer" followed by a space and JWT token.
    //
    // @tag.name dogs
    // @tag.description Dogs are cool
    //
    // @tag.name cats
    // @tag.description Cats are the devil
    func main() {}
  2. Run swag init against it.
  3. Open the generated swagger.json / swagger.yaml and look at the tags key: it's an empty array, even though dogs and cats are both declared with descriptions right there in the source.

Test Setup
Added testdata/tags3/main.go, trimmed to just what's needed to isolate the bug: a general API info block with @title/@version/@BasePath, one @securityDefinitions.apikey scheme, two @tag.name/@tag.description pairs (dogs, cats) — trying to reflect as closely as possible the structure of a typical real-world main.go file.

Added TestApiParseTag_AfterSecurityDefinitions in parser_test.go (parser_test.go:3136), right after TestParseTagMarkdownDescription:

func TestApiParseTag_AfterSecurityDefinitions(t *testing.T) {
	t.Parallel()

	// @tag.name/@tag.description must still be parsed when they follow a
	// @securityDefinitions.* block in the same general API info comment group.
	searchDir := "testdata/tags3"
	p := New()
	p.PropNamingStrategy = PascalCase
	err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
	assert.NoError(t, err)

	assert.Len(t, p.swagger.Tags, 2)

	dogs := p.swagger.Tags[0]
	assert.Equal(t, "dogs", dogs.TagProps.Name)
	assert.Equal(t, "Dogs are cool", dogs.TagProps.Description)

	cats := p.swagger.Tags[1]
	assert.Equal(t, "cats", cats.TagProps.Name)
	assert.Equal(t, "Cats are the devil", cats.TagProps.Description)
}

Before the fix, this test failed because the list of tags was empty (p.swagger.Tags).
It is important to note that if the tags come BEFORE the security attributes, the test passes correctly without any changes to the code.

Solution
Root cause is in parseSecAttributes (parser.go:764), the function that parses one @securityDefinitions.* block. It's called from the general-info parser's outer loop with the loop's own line index, passed by pointer:

// parser.go:651-655
case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr:
    scheme, err := parseSecAttributes(attribute, comments, &line)

parseSecAttributes then runs its own scan (the loopline loop, parser.go:795-844) over that same shared slice, advancing the shared index while looking for the attributes that belong to that security scheme (@in, @name, @tokenUrl, @authorizationUrl, @scope.*, @description, @x-*). The only thing that stopped the scan was running into another @securityDefinitions.* line. Any other, unrelated attribute — @tag.name, @tag.description, @contact.*, @license.*, @externalDocs.* — fell through unrecognized and was silently skipped, with the shared index advancing right past it. With no second security block to act as a stop sign, the scan ran to the end of the comment group and quietly consumed everything after it, tags included, before the outer general-info loop ever got to see those lines.

The fix has two parts, both inside that same loopline loop:

  1. Stop the inner scan on any unrecognized attribute, not only on another @securityDefinitions.* line — back the shared index up one line and break, handing control back to the outer general-info loop so it can process that line itself (where @tag.name, @contact.name, etc. are already handled correctly).
  2. Add the missing continue to the existing @description branch. This surfaced while implementing (1): the @description branch never had a continue, relying on the old code having nothing after it besides the "next security block" check. Once (1) added a fallback after that check, a line that had already been handled by the @description branch fell through into the new fallback too, ending the scan one line early.

diff for the suggested change:

 		// Not mandatory field
 		if securityAttr == descriptionAttr {
 			if description != "" {
 				description += "\n"
 			}
 			description += value
+			continue
 		}

-		// next securityDefinitions
-		if strings.Index(securityAttr, "@securitydefinitions.") == 0 {
-			// Go back to the previous line and break
-			*index--
-
-			break
-		}
+		// Either a new @securityDefinitions block starts here, or the attribute
+		// is unrecognized and belongs to the outer general-info parser.
+		// In both cases, go back to the previous line and stop.
+		*index--
+		break
 	}

The change is minimal and contained to parseSecAttributes. Verified with make test: the full suite across the swag, gen, and format packages passes, including the new regression test and the two tests that an earlier, incomplete version of this fix had broken.

Expected behavior
@tag.name / @tag.description pairs are parsed into swagger.Tags regardless of what other general-info annotations — security definitions, contact, license, external docs — precede them in the same comment block.

Local setup information:

  • Swag version: 1.16.7
  • Go version: 1.26.4
  • OS: MacOS Tahoe 26.5.2

@exageraldo

exageraldo commented Jul 15, 2026

Copy link
Copy Markdown
Author

In a slightly more comprehensive test, it was shown that the problem lies after the security attributes. For example, let's consider the test for this main.go file:

package main

// @title Some API Title
// @version 0.0.1
// @BasePath /
// @schemes http https
//
// @securityDefinitions.apikey  Bearer
// @in                          header
// @name                        Authorization
// @description                 Type "Bearer" followed by a space and JWT token.
//
// @tag.name dogs
// @tag.description Dogs are cool
//
// @tag.name cats
// @tag.description Cats are the devil
// @tag.docs.url https://example.com/cats
// @tag.docs.description Everything about cats
//
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @termsOfService http://swagger.io/terms/
//
// @security Bearer
//
// @externalDocs.description Some description here
// @externalDocs.url http://some.url.com/path/here/
//
// @x-foo "some value"
func main() {}

When running the following test, all conditions fail:

func TestApiParse_R(t *testing.T) {
	t.Parallel()

	searchDir := "testdata/tags3"
	p := New()
	p.PropNamingStrategy = PascalCase
	err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
	assert.NoError(t, err)

	assert.Equal(t, "API Support", p.swagger.Info.Contact.Name)
	assert.Equal(t, "http://www.swagger.io/support", p.swagger.Info.Contact.URL)
	assert.Equal(t, "support@swagger.io", p.swagger.Info.Contact.Email)

	if assert.NotNil(t, p.swagger.Info.License) {
		assert.Equal(t, "Apache 2.0", p.swagger.Info.License.Name)
		assert.Equal(t, "http://www.apache.org/licenses/LICENSE-2.0.html", p.swagger.Info.License.URL)
	}

	assert.Equal(t, "http://swagger.io/terms/", p.swagger.Info.TermsOfService)

	assert.Len(t, p.swagger.Security, 1)

	if assert.NotNil(t, p.swagger.ExternalDocs) {
		assert.Equal(t, "Some description here", p.swagger.ExternalDocs.Description)
		assert.Equal(t, "http://some.url.com/path/here/", p.swagger.ExternalDocs.URL)
	}

	// @x-foo is a top-level extension; it must not end up attached to the
	// Bearer security scheme instead.
	topLevel, onDoc := p.swagger.Extensions.GetString("x-foo")
	assert.True(t, onDoc, "@x-foo missing from the top-level document")
	assert.Equal(t, "some value", topLevel)

	_, onScheme := p.swagger.SecurityDefinitions["Bearer"].Extensions.GetString("x-foo")
	assert.False(t, onScheme, "@x-foo ended up attached to the Bearer security scheme instead of the top-level document")

	assert.Len(t, p.swagger.Tags, 2)

	dogs := p.swagger.Tags[0]
	assert.Equal(t, "dogs", dogs.TagProps.Name)
	assert.Equal(t, "Dogs are cool", dogs.TagProps.Description)

	cats := p.swagger.Tags[1]
	assert.Equal(t, "cats", cats.TagProps.Name)
	assert.Equal(t, "Cats are the devil", cats.TagProps.Description)
	if assert.NotNil(t, cats.TagProps.ExternalDocs) {
		assert.Equal(t, "https://example.com/cats", cats.TagProps.ExternalDocs.URL)
		assert.Equal(t, "Everything about cats", cats.TagProps.ExternalDocs.Description)
	}
}

Test output:

=== RUN   TestApiParseTag_AfterSecurityDefinitions
=== PAUSE TestApiParseTag_AfterSecurityDefinitions
=== CONT  TestApiParseTag_AfterSecurityDefinitions
2026/07/15 14:54:56 Generate general API Info, search dir:testdata/tags3
    parser_test.go:4658: 
        	Error Trace:	/swag/parser_test.go:4658
        	Error:      	Not equal: 
        	            	expected: "API Support"
        	            	actual  : ""
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-API Support
        	            	+
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4659: 
        	Error Trace:	/swag/parser_test.go:4659
        	Error:      	Not equal: 
        	            	expected: "http://www.swagger.io/support"
        	            	actual  : ""
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-http://www.swagger.io/support
        	            	+
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4660: 
        	Error Trace:	/swag/parser_test.go:4660
        	Error:      	Not equal: 
        	            	expected: "support@swagger.io"
        	            	actual  : ""
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-support@swagger.io
        	            	+
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4662: 
        	Error Trace:	/swag/parser_test.go:4662
        	Error:      	Expected value not to be nil.
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4667: 
        	Error Trace:	/swag/parser_test.go:4667
        	Error:      	Not equal: 
        	            	expected: "http://swagger.io/terms/"
        	            	actual  : ""
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-http://swagger.io/terms/
        	            	+
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4669: 
        	Error Trace:	/swag/parser_test.go:4669
        	Error:      	"[]" should have 1 item(s), but has 0
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4671: 
        	Error Trace:	/swag/parser_test.go:4671
        	Error:      	Expected value not to be nil.
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4679: 
        	Error Trace:	/swag/parser_test.go:4679
        	Error:      	Should be true
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
        	Messages:   	@x-foo missing from the top-level document
    parser_test.go:4680: 
        	Error Trace:	/swag/parser_test.go:4680
        	Error:      	Not equal: 
        	            	expected: "some value"
        	            	actual  : ""
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-some value
        	            	+
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
    parser_test.go:4683: 
        	Error Trace:	/swag/parser_test.go:4683
        	Error:      	Should be false
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
        	Messages:   	@x-foo ended up attached to the Bearer security scheme instead of the top-level document
    parser_test.go:4685: 
        	Error Trace:	/swag/parser_test.go:4685
        	Error:      	"[]" should have 2 item(s), but has 0
        	Test:       	TestApiParseTag_AfterSecurityDefinitions
--- FAIL: TestApiParseTag_AfterSecurityDefinitions (0.18s)
panic: runtime error: index out of range [0] with length 0 [recovered, repanicked]

But if we move @securityDefinitions (and its attributes) down - after @x-foo - the entire test passes.
The suggested fix allows the tests to run regardless of the position of the security attributes.

@exageraldo exageraldo changed the title Fix Tags parse after Security attrs Fix security attrs parse Jul 15, 2026
@gitguardian

gitguardian Bot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic Password fe9cfcf parser_test.go View secret
- - Generic Password 7119635 parser_test.go View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@exageraldo

Copy link
Copy Markdown
Author

a false positive 👆🏻

@exageraldo

Copy link
Copy Markdown
Author

I tried changing a few values, but unfortunately it didn't work.

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