Repository: asciidoctor/asciidoctor (Ruby)

Issue:
counter and counter2 attribute directives should not override locked attributes
`counter` has a loophole that allows to override any attribute (with an arbitrary value) even when this attribute is locked.

Here's a simple reproduction case:

```console
$ asciidoctor -v
Asciidoctor 2.0.12 [https://asciidoctor.org]
Runtime Environment (ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]) (lc:UTF-8 fs:UTF-8 in:UTF-8 ex:UTF-8)
```

```console
$ echo '{counter2:imagesdir:/noop} image:hello.png[]' | asciidoctor -a imagesdir=/img -s -
<div class="paragraph">
<p> <span class="image"><img src="/noop/hello.png" alt="hello"></span></p>
</div>
```

As you can see above, the `imagesdir` attribute is locked via the CLI (using `-a imagesdir=/img`) but the `counter2` attribute reference overrides the value from `/img` to `/noop`.
As a result, the `src` attribute on the `<image>` element is `/noop/hello.png` instead of `/img/hello.png`.



Discussion:

Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index bc80b91fc6..dbd8274cd2 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -17,6 +17,7 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[
 
 Bug Fixes::
 
+  * Don't allow counter and counter2 directives in attribute reference to override locked attributes (#3939)
   * Update default stylesheet to remove dash in front of cite on nested quote block (#3847)
   * Don't escape hyphen in manname in manpage output
   * Remove extra .sp before content of verse block in manpage output
diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb
index ca2abfdd82..b71443dd08 100644
--- a/lib/asciidoctor/document.rb
+++ b/lib/asciidoctor/document.rb
@@ -562,7 +562,9 @@ def parsed?
   # returns the next number in the sequence for the specified counter
   def counter name, seed = nil
     return @parent_document.counter name, seed if @parent_document
-    if (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
+    if attribute_locked? name
+      @attributes[name]
+    elsif (attr_seed = !(attr_val = @attributes[name]).nil_or_empty?) && (@counters.key? name)
       @attributes[name] = @counters[name] = Helpers.nextval attr_val
     elsif seed
       @attributes[name] = @counters[name] = seed == seed.to_i.to_s ? seed.to_i : seed


Test patch:
diff --git a/test/attributes_test.rb b/test/attributes_test.rb
index b81c723b4b..be4f744ef4 100644
--- a/test/attributes_test.rb
+++ b/test/attributes_test.rb
@@ -1122,6 +1122,46 @@
       assert_xpath '//div[@class="title"][text() = "Figure 3. Title for Baz"]', output, 1
       assert_xpath '//div[@class="title"][text() = "Figure 4. Title for Qux"]', output, 1
     end
+
+    test 'should not allow counter to modify locked attribute' do
+      input = <<~'EOS'
+      {counter:foo:baz} is still {foo}
+      EOS
+
+      output = convert_string_to_embedded input, :attributes => { 'foo' => 'bar' }
+      assert_xpath '//p[text()="bar is still bar"]', output, 1
+    end
+
+    test 'should not allow counter2 to modify locked attribute' do
+      input = <<~'EOS'
+      {counter2:foo:baz}{foo}
+      EOS
+
+      output = convert_string_to_embedded input, :attributes => { 'foo' => 'bar' }
+      assert_xpath '//p[text()="bar"]', output, 1
+    end
+
+    test 'should not allow counter to modify built-in locked attribute' do
+      input = <<~'EOS'
+      {counter:max-include-depth:128} is still {max-include-depth}
+      EOS
+
+      doc = document_from_string input, standalone: false
+      output = doc.convert
+      assert_xpath '//p[text()="64 is still 64"]', output, 1
+      assert_equal 64, doc.attributes['max-include-depth']
+    end
+
+    test 'should not allow counter2 to modify built-in locked attribute' do
+      input = <<~'EOS'
+      {counter2:max-include-depth:128}{max-include-depth}
+      EOS
+
+      doc = document_from_string input, standalone: false
+      output = doc.convert
+      assert_xpath '//p[text()="64"]', output, 1
+      assert_equal 64, doc.attributes['max-include-depth']
+    end
   end
 
   context 'Block attributes' do